diff --git a/.env.example b/.env.example
index c03638072..b3e592bdf 100644
--- a/.env.example
+++ b/.env.example
@@ -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=
\ No newline at end of file
+LANGFLOW_SUPERUSER_PASSWORD=
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..d81146d19
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -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
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e009534c2..bc2e193c6 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -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
diff --git a/.gitignore b/.gitignore
index 156f44394..9b8ec1cc1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -254,3 +254,4 @@ langflow.db
/tmp/*
src/backend/langflow/frontend/
+.docker
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 2fe79291e..65d4253f0 100644
--- a/Makefile
+++ b/Makefile
@@ -61,7 +61,7 @@ frontendc:
make run_frontend
install_backend:
- poetry install
+ poetry install --extras deploy
backend:
make install_backend
diff --git a/base.Dockerfile b/base.Dockerfile
new file mode 100644
index 000000000..c76bbbcda
--- /dev/null
+++ b/base.Dockerfile
@@ -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=
+
diff --git a/deploy/.env.example b/deploy/.env.example
new file mode 100644
index 000000000..e67307406
--- /dev/null
+++ b/deploy/.env.example
@@ -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
\ No newline at end of file
diff --git a/deploy/.gitignore b/deploy/.gitignore
new file mode 100644
index 000000000..fdbcefec6
--- /dev/null
+++ b/deploy/.gitignore
@@ -0,0 +1 @@
+pgadmin
\ No newline at end of file
diff --git a/deploy/base.Dockerfile b/deploy/base.Dockerfile
new file mode 100644
index 000000000..323663283
--- /dev/null
+++ b/deploy/base.Dockerfile
@@ -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
diff --git a/deploy/docker-compose.override.yml b/deploy/docker-compose.override.yml
new file mode 100644
index 000000000..e5c0d9dd7
--- /dev/null
+++ b/deploy/docker-compose.override.yml
@@ -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
diff --git a/deploy/docker-compose.test.yml b/deploy/docker-compose.test.yml
new file mode 100644
index 000000000..829aae883
--- /dev/null
+++ b/deploy/docker-compose.test.yml
@@ -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
diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
new file mode 100644
index 000000000..4eda0dc37
--- /dev/null
+++ b/deploy/docker-compose.yml
@@ -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}
diff --git a/deploy/prometheus.yml b/deploy/prometheus.yml
new file mode 100644
index 000000000..1d53d5b5f
--- /dev/null
+++ b/deploy/prometheus.yml
@@ -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"]
diff --git a/deploy/scripts/.gitignore b/deploy/scripts/.gitignore
new file mode 100644
index 000000000..c60caa840
--- /dev/null
+++ b/deploy/scripts/.gitignore
@@ -0,0 +1,5 @@
+# terraform
+*.tfstate
+*.tfstate.backup
+*.tfvars
+.terraform/
\ No newline at end of file
diff --git a/deploy/scripts/build-push.sh b/deploy/scripts/build-push.sh
new file mode 100755
index 000000000..287b99c67
--- /dev/null
+++ b/deploy/scripts/build-push.sh
@@ -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
\ No newline at end of file
diff --git a/deploy/scripts/build.sh b/deploy/scripts/build.sh
new file mode 100755
index 000000000..4d8c2743a
--- /dev/null
+++ b/deploy/scripts/build.sh
@@ -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
\ No newline at end of file
diff --git a/deploy/scripts/terraform/.terraform.lock.hcl b/deploy/scripts/terraform/.terraform.lock.hcl
new file mode 100644
index 000000000..3a247d856
--- /dev/null
+++ b/deploy/scripts/terraform/.terraform.lock.hcl
@@ -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",
+ ]
+}
diff --git a/deploy/scripts/terraform/main.tf b/deploy/scripts/terraform/main.tf
new file mode 100644
index 000000000..8c103d00b
--- /dev/null
+++ b/deploy/scripts/terraform/main.tf
@@ -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]
+ }
+}
diff --git a/deploy/scripts/terraform/modules/docker-swarm/main.tf b/deploy/scripts/terraform/modules/docker-swarm/main.tf
new file mode 100644
index 000000000..c29f572e9
--- /dev/null
+++ b/deploy/scripts/terraform/modules/docker-swarm/main.tf
@@ -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
+}
diff --git a/deploy/scripts/terraform/outputs.tf b/deploy/scripts/terraform/outputs.tf
new file mode 100644
index 000000000..30303fad9
--- /dev/null
+++ b/deploy/scripts/terraform/outputs.tf
@@ -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
+}
+
diff --git a/deploy/scripts/terraform/variables.tf b/deploy/scripts/terraform/variables.tf
new file mode 100644
index 000000000..e73528c88
--- /dev/null
+++ b/deploy/scripts/terraform/variables.tf
@@ -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
+}
diff --git a/deploy/startup-backend.sh b/deploy/startup-backend.sh
new file mode 100755
index 000000000..75c8002a5
--- /dev/null
+++ b/deploy/startup-backend.sh
@@ -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
+
diff --git a/docs/docs/guidelines/login.mdx b/docs/docs/guidelines/login.mdx
new file mode 100644
index 000000000..85ca1371f
--- /dev/null
+++ b/docs/docs/guidelines/login.mdx
@@ -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
+```
+
+
+ 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.
+
+
+### _`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.
+
+
+
+## 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.
+
+
+
+By clicking on **Profile Settings**, the user is taken to the profile settings page, where they can change their password and their profile picture.
+
+
+
+By clicking on **Admin Page**, the superuser is taken to the admin page, where they can manage users and groups.
+
+
diff --git a/docs/docs/guides/superuser.mdx b/docs/docs/guides/superuser.mdx
new file mode 100644
index 000000000..04e0f96af
--- /dev/null
+++ b/docs/docs/guides/superuser.mdx
@@ -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,
diff --git a/docs/sidebars.js b/docs/sidebars.js
index bc02f7407..166d72f22 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -16,6 +16,8 @@ module.exports = {
label: "Guidelines",
collapsed: false,
items: [
+ "guidelines/login",
+ // "guidelines/api",
"guidelines/components",
"guidelines/features",
"guidelines/collection",
diff --git a/docs/static/img/admin-page.png b/docs/static/img/admin-page.png
new file mode 100644
index 000000000..aa23164de
Binary files /dev/null and b/docs/static/img/admin-page.png differ
diff --git a/docs/static/img/my-account.png b/docs/static/img/my-account.png
new file mode 100644
index 000000000..c8b887ca1
Binary files /dev/null and b/docs/static/img/my-account.png differ
diff --git a/docs/static/img/profile-settings.png b/docs/static/img/profile-settings.png
new file mode 100644
index 000000000..77077d463
Binary files /dev/null and b/docs/static/img/profile-settings.png differ
diff --git a/docs/static/img/sign-up.png b/docs/static/img/sign-up.png
new file mode 100644
index 000000000..3ffdd1793
Binary files /dev/null and b/docs/static/img/sign-up.png differ
diff --git a/poetry.lock b/poetry.lock
index bbfb4f2c9..8fb013726 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -163,6 +163,20 @@ typing-extensions = ">=4"
[package.extras]
tz = ["python-dateutil"]
+[[package]]
+name = "amqp"
+version = "5.1.1"
+description = "Low-level AMQP client for Python (fork of amqplib)."
+optional = true
+python-versions = ">=3.6"
+files = [
+ {file = "amqp-5.1.1-py3-none-any.whl", hash = "sha256:6f0956d2c23d8fa6e7691934d8c3930eadb44972cbbd1a7ae3a520f735d43359"},
+ {file = "amqp-5.1.1.tar.gz", hash = "sha256:2c1b13fecc0893e946c65cbd5f36427861cffa4ea2201d8f6fca22e2a373b5e2"},
+]
+
+[package.dependencies]
+vine = ">=5.0.0"
+
[[package]]
name = "anthropic"
version = "0.3.3"
@@ -376,6 +390,17 @@ soupsieve = ">1.2"
html5lib = ["html5lib"]
lxml = ["lxml"]
+[[package]]
+name = "billiard"
+version = "4.1.0"
+description = "Python multiprocessing fork with improvements and bugfixes"
+optional = true
+python-versions = ">=3.7"
+files = [
+ {file = "billiard-4.1.0-py3-none-any.whl", hash = "sha256:0f50d6be051c6b2b75bfbc8bfd85af195c5739c281d3f5b86a5640c65563614a"},
+ {file = "billiard-4.1.0.tar.gz", hash = "sha256:1ad2eeae8e28053d729ba3373d34d9d6e210f6e4d8bf0a9c64f92bd053f1edf5"},
+]
+
[[package]]
name = "black"
version = "23.9.1"
@@ -422,6 +447,109 @@ d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
+[[package]]
+name = "blinker"
+version = "1.6.2"
+description = "Fast, simple object-to-object and broadcast signaling"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "blinker-1.6.2-py3-none-any.whl", hash = "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0"},
+ {file = "blinker-1.6.2.tar.gz", hash = "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213"},
+]
+
+[[package]]
+name = "brotli"
+version = "1.1.0"
+description = "Python bindings for the Brotli compression library"
+optional = false
+python-versions = "*"
+files = [
+ {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"},
+ {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"},
+ {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"},
+ {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"},
+ {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"},
+ {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"},
+ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"},
+ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"},
+ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"},
+ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"},
+ {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"},
+ {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"},
+ {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"},
+ {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"},
+ {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"},
+ {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"},
+ {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"},
+ {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"},
+ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"},
+ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"},
+ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"},
+ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"},
+ {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"},
+ {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"},
+ {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"},
+ {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"},
+ {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"},
+ {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"},
+ {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"},
+ {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"},
+ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"},
+ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"},
+ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"},
+ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"},
+ {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"},
+ {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"},
+ {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"},
+ {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"},
+ {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"},
+ {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"},
+ {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"},
+ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"},
+ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"},
+ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"},
+ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"},
+ {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"},
+ {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"},
+ {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"},
+ {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"},
+ {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"},
+ {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"},
+ {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"},
+ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"},
+ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"},
+ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"},
+ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"},
+ {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"},
+ {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"},
+ {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"},
+ {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"},
+ {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"},
+ {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"},
+ {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"},
+ {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"},
+ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"},
+ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"},
+ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"},
+ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"},
+ {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"},
+ {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"},
+ {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"},
+ {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"},
+ {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"},
+ {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"},
+ {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"},
+ {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"},
+ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"},
+ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"},
+ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"},
+ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"},
+ {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"},
+ {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"},
+ {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"},
+]
+
[[package]]
name = "cachetools"
version = "5.3.1"
@@ -433,6 +561,62 @@ files = [
{file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"},
]
+[[package]]
+name = "celery"
+version = "5.3.4"
+description = "Distributed Task Queue."
+optional = true
+python-versions = ">=3.8"
+files = [
+ {file = "celery-5.3.4-py3-none-any.whl", hash = "sha256:1e6ed40af72695464ce98ca2c201ad0ef8fd192246f6c9eac8bba343b980ad34"},
+ {file = "celery-5.3.4.tar.gz", hash = "sha256:9023df6a8962da79eb30c0c84d5f4863d9793a466354cc931d7f72423996de28"},
+]
+
+[package.dependencies]
+billiard = ">=4.1.0,<5.0"
+click = ">=8.1.2,<9.0"
+click-didyoumean = ">=0.3.0"
+click-plugins = ">=1.1.1"
+click-repl = ">=0.2.0"
+kombu = ">=5.3.2,<6.0"
+python-dateutil = ">=2.8.2"
+redis = {version = ">=4.5.2,<4.5.5 || >4.5.5,<5.0.0", optional = true, markers = "extra == \"redis\""}
+tzdata = ">=2022.7"
+vine = ">=5.0.0,<6.0"
+
+[package.extras]
+arangodb = ["pyArango (>=2.0.2)"]
+auth = ["cryptography (==41.0.3)"]
+azureblockblob = ["azure-storage-blob (>=12.15.0)"]
+brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"]
+cassandra = ["cassandra-driver (>=3.25.0,<4)"]
+consul = ["python-consul2 (==0.1.5)"]
+cosmosdbsql = ["pydocumentdb (==2.3.5)"]
+couchbase = ["couchbase (>=3.0.0)"]
+couchdb = ["pycouchdb (==1.14.2)"]
+django = ["Django (>=2.2.28)"]
+dynamodb = ["boto3 (>=1.26.143)"]
+elasticsearch = ["elasticsearch (<8.0)"]
+eventlet = ["eventlet (>=0.32.0)"]
+gevent = ["gevent (>=1.5.0)"]
+librabbitmq = ["librabbitmq (>=2.0.0)"]
+memcache = ["pylibmc (==1.6.3)"]
+mongodb = ["pymongo[srv] (>=4.0.2)"]
+msgpack = ["msgpack (==1.0.5)"]
+pymemcache = ["python-memcached (==1.59)"]
+pyro = ["pyro4 (==4.82)"]
+pytest = ["pytest-celery (==0.0.0)"]
+redis = ["redis (>=4.5.2,!=4.5.5,<5.0.0)"]
+s3 = ["boto3 (>=1.26.143)"]
+slmq = ["softlayer-messaging (>=1.0.3)"]
+solar = ["ephem (==4.1.4)"]
+sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"]
+sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.0)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"]
+tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"]
+yaml = ["PyYAML (>=3.10)"]
+zookeeper = ["kazoo (>=1.3.1)"]
+zstd = ["zstandard (==0.21.0)"]
+
[[package]]
name = "certifi"
version = "2023.7.22"
@@ -658,6 +842,20 @@ files = [
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
+[[package]]
+name = "click-didyoumean"
+version = "0.3.0"
+description = "Enables git-like *did-you-mean* feature in click"
+optional = true
+python-versions = ">=3.6.2,<4.0.0"
+files = [
+ {file = "click-didyoumean-0.3.0.tar.gz", hash = "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"},
+ {file = "click_didyoumean-0.3.0-py3-none-any.whl", hash = "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667"},
+]
+
+[package.dependencies]
+click = ">=7"
+
[[package]]
name = "click-log"
version = "0.4.0"
@@ -672,78 +870,113 @@ files = [
[package.dependencies]
click = "*"
+[[package]]
+name = "click-plugins"
+version = "1.1.1"
+description = "An extension module for click to enable registering CLI commands via setuptools entry-points."
+optional = true
+python-versions = "*"
+files = [
+ {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"},
+ {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"},
+]
+
+[package.dependencies]
+click = ">=4.0"
+
+[package.extras]
+dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"]
+
+[[package]]
+name = "click-repl"
+version = "0.3.0"
+description = "REPL plugin for Click"
+optional = true
+python-versions = ">=3.6"
+files = [
+ {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"},
+ {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"},
+]
+
+[package.dependencies]
+click = ">=7.0"
+prompt-toolkit = ">=3.0.36"
+
+[package.extras]
+testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"]
+
[[package]]
name = "clickhouse-connect"
-version = "0.6.13"
+version = "0.6.14"
description = "ClickHouse Database Core Driver for Python, Pandas, and Superset"
optional = false
python-versions = "~=3.7"
files = [
- {file = "clickhouse-connect-0.6.13.tar.gz", hash = "sha256:affec3369f486db1d17fc5447fef541a9a70df09c03e0ed4af6b328b6e6a4827"},
- {file = "clickhouse_connect-0.6.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3538dd61054af1d7af712f0203265969e351ca6e21a138c5cc967a4d63bc684"},
- {file = "clickhouse_connect-0.6.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:08eac69a47c5d5bba727a475593662d5583a44a2168cb34c7e9727b0949edf05"},
- {file = "clickhouse_connect-0.6.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34cad54d3af842f98db7eda9a0fd4c0201383c6e6316b3db364a5e62957d3f92"},
- {file = "clickhouse_connect-0.6.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9ecd833018ae61b1b5c23f1856e63112eb5a9240e30ae79e7cd63032bcc7683"},
- {file = "clickhouse_connect-0.6.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52cf7b75f4c9a215d1cde4bdab079b463f75edecf311a9f34248340de8c58ab8"},
- {file = "clickhouse_connect-0.6.13-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a5febb40a3220c2a3aaddaffe3cbc0c2c668dbf58d68875c5236551a43fb54f2"},
- {file = "clickhouse_connect-0.6.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:71072c28ef2d1d5c41f23b898014c8c491c54a745a13a545cd18e60d44d7276e"},
- {file = "clickhouse_connect-0.6.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:47135049bacfdb67272df3e988458fc8cb3ca31df7fd020d7c28801272f319ae"},
- {file = "clickhouse_connect-0.6.13-cp310-cp310-win32.whl", hash = "sha256:3ad88ca2b34d330513ac8e76f74dca4c1879a6ad81f6ef331e626400c3b9ff62"},
- {file = "clickhouse_connect-0.6.13-cp310-cp310-win_amd64.whl", hash = "sha256:092ddacb003a193f17ee3dbb6ac5df8397730154df95c668342e419e35dc42df"},
- {file = "clickhouse_connect-0.6.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77efffc06dede8ac479678420cc5e15b2be28b73119a458766047b964d31a36"},
- {file = "clickhouse_connect-0.6.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:39669fc21d1ab76fe6042511e424a00c7b5b731db2ff59c7a6222f15a65a77b0"},
- {file = "clickhouse_connect-0.6.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25005cf2c44bc46fa11b3e95de53830c172bf6f1cc512a2628bd62861b60777a"},
- {file = "clickhouse_connect-0.6.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d049b3c88466db6806d9b301436ddb69024df8cb1c14e5b21fab4517b67094"},
- {file = "clickhouse_connect-0.6.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c329296469651124fb9787b6c060c22c05dfad1129f8e3683721cfe98b2b6574"},
- {file = "clickhouse_connect-0.6.13-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bb3a8f985626a8c5fc8d12e7e16e1f1774ef5c574faf10432e41a2e97ec2684"},
- {file = "clickhouse_connect-0.6.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ed964a08ebd8ce4ddf44686efcf706fe6089a0b439813a4796c6cf3fc4f5ab11"},
- {file = "clickhouse_connect-0.6.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9f4d3e17fa7c0b35e420450b18ad9fdde70f92635c12a69e5a18c699720c0442"},
- {file = "clickhouse_connect-0.6.13-cp311-cp311-win32.whl", hash = "sha256:fcab92aadd62ecc4dbe9f841c72e714f519f5251f45bf04f761504cd419abff3"},
- {file = "clickhouse_connect-0.6.13-cp311-cp311-win_amd64.whl", hash = "sha256:a312939b94ec2dd49556cbef880804898fc14d5ca52aeb3d00b9905047ca64e0"},
- {file = "clickhouse_connect-0.6.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e3fef39ce56c5550ebc701fc439ca517eca10c57166216ebd37b664a5352be7"},
- {file = "clickhouse_connect-0.6.13-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1841a5f1c1298907c42e23fef1f4bdcdb6c7c40b2844469ccd4780e8f7cc16f"},
- {file = "clickhouse_connect-0.6.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:120618bf66df0908c0b8b5e8ea07c068d98032a9f498c832b1b02aa2cef91474"},
- {file = "clickhouse_connect-0.6.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3a7c34efd02e68445cc6bf13f6de9c4b294112351e79a386cbd65c13fa08965"},
- {file = "clickhouse_connect-0.6.13-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a3f6a6975759b0b3aa8a4a6e05533bdce234a16804e69282bb0c01a5a37851a"},
- {file = "clickhouse_connect-0.6.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6317a3ee357ce7129de18ce344792c545f4b28a190a47bf4047414c9b12dfaf2"},
- {file = "clickhouse_connect-0.6.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b0d07ae7b0d7ede51a336dfee5bbfdbf123957a50e9452d57f58b0c803517f21"},
- {file = "clickhouse_connect-0.6.13-cp37-cp37m-win32.whl", hash = "sha256:ef0ca69741762e512a6f1882d53bb1e607e2e04d24db995c8c0b3409943891b4"},
- {file = "clickhouse_connect-0.6.13-cp37-cp37m-win_amd64.whl", hash = "sha256:a5ba1ed57f4d8a47a87e353756746d4199527568b962931f5d6b9388ba72d8f3"},
- {file = "clickhouse_connect-0.6.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03f6738e9c737531d7ec67611cb476ca8b817b583e5f46349fc63b7357cdda4e"},
- {file = "clickhouse_connect-0.6.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:484b528d3c79f07f9c8a6586fb254578b8f8140a5a309820737f5017e9edd02e"},
- {file = "clickhouse_connect-0.6.13-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0c5589016da8a0a1c21d2461a98a6cab7c22633423735647632f5b5c0de7109"},
- {file = "clickhouse_connect-0.6.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81495c2d12a6420c1f3ee5d0cf3996329dbfd60e12e53905fd9da577b992c0c8"},
- {file = "clickhouse_connect-0.6.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2239972b84dd33baf7cd552b53dec024716b87fbebb3a2ff741398a96fa15777"},
- {file = "clickhouse_connect-0.6.13-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9737310a7466f5ac10077ecbe10237e816a12a8cc4c651c2be82eaf25895042b"},
- {file = "clickhouse_connect-0.6.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a229d67f1dec0351faf7634f523c8fcf78b602ee02b0e25c93f4d5018147d4f4"},
- {file = "clickhouse_connect-0.6.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a996fdf7d8e6bc18c9365fc06bd429418510cf63476a056e36afee505f570dad"},
- {file = "clickhouse_connect-0.6.13-cp38-cp38-win32.whl", hash = "sha256:56e51196caeb6b7f7b62af8e58ce1a653e29c694e4cf0964907ca7b42b32e7f0"},
- {file = "clickhouse_connect-0.6.13-cp38-cp38-win_amd64.whl", hash = "sha256:a7b0cfec5eff21ec81f713de77a136d74f9336a20da5096b7a046b27ca1014b7"},
- {file = "clickhouse_connect-0.6.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:909a6d4406a9320d85d7b71a131e68f9289ec66453ba7bf14e9a110db5f18e3d"},
- {file = "clickhouse_connect-0.6.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7845ad20a8fba276dff1670c1bbedc3dc2c4d560fa486d8960fdd217b188099"},
- {file = "clickhouse_connect-0.6.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d5eb2036b740496a8d9f871cf928493fbc7d9edfc543e16f5db56ef359d468"},
- {file = "clickhouse_connect-0.6.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe0b889587bfe2962cbfe1b0a908f8bafee332a8f955f38fa307561dd73a962c"},
- {file = "clickhouse_connect-0.6.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0414e6a2b2e19ad4165cb01503086d229473465217be86a7824cf9f4a2d3964"},
- {file = "clickhouse_connect-0.6.13-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9065b9a7def6e1f7bf74e9c81825927f0b9147fcf1f0d172533552d931311279"},
- {file = "clickhouse_connect-0.6.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2b7b4e29c781d8feb87cac6133ef8a20b59cd70e600d9f37875dd74384c8b792"},
- {file = "clickhouse_connect-0.6.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:66778f60bc66891ff375aa28447846a7f176743800f8fbec23001b05b8912020"},
- {file = "clickhouse_connect-0.6.13-cp39-cp39-win32.whl", hash = "sha256:fa1f34d5b56f86417be2490e9cb22b1b27ff788e569636fdae1027d66816c2b3"},
- {file = "clickhouse_connect-0.6.13-cp39-cp39-win_amd64.whl", hash = "sha256:b66574c7dc2a805071b582a66a0a46c92d3b3e1e897336cc32c5e0054204b401"},
- {file = "clickhouse_connect-0.6.13-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c87224b4ecec054395759cfbe5aab56f1728a14fb382178b0ae29c06068a1487"},
- {file = "clickhouse_connect-0.6.13-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2f416e38cc935f982443a21642eac3d09e56f32fffa7a2ec90f36175bcd92ee"},
- {file = "clickhouse_connect-0.6.13-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caffe66da4d9f8777b72ac93884ce7726a08a154527a444aa3e9913f4f44523f"},
- {file = "clickhouse_connect-0.6.13-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdcb082acf5a1b754ea360e5df0f8805c17df1db9cdc47dcf10c7cfc2f0ea4d0"},
- {file = "clickhouse_connect-0.6.13-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:bebfd2d9fbaf67a84c78cce0f48cac3224c9eef62de7334e57dd32d38399bec9"},
- {file = "clickhouse_connect-0.6.13-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:98ef70b8e63f3145603a570bd479bc66f1269be18baec5df71c2f58959c345c1"},
- {file = "clickhouse_connect-0.6.13-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd1ea8c2b694bb441371c7c964a9ff9b92489863087a306419fa8680ff189056"},
- {file = "clickhouse_connect-0.6.13-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba59dead59a7f32522f502d3061fa70f0978b0a03ebe7992dcea53be4a5f587e"},
- {file = "clickhouse_connect-0.6.13-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c40ac936bb35bbf32aca6881755e392ba2ef3eece028e7b24c40498a662e9bfe"},
- {file = "clickhouse_connect-0.6.13-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:70397690dc7574d2ff546f311bd2243177925cc74f0df2613d2985a0da8c7516"},
- {file = "clickhouse_connect-0.6.13-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d3ec25049fff943844e24a65112d15ed4d006f7c528362dd997844e4bcfce7cd"},
- {file = "clickhouse_connect-0.6.13-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b8614233a0ff7a3b469876b95dcf4557ed8fbc15c6997d49ddbde323537161"},
- {file = "clickhouse_connect-0.6.13-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bf02bc20bf308853d6d382dac0d3347e4db2e76987610a098c7b1b207f355"},
- {file = "clickhouse_connect-0.6.13-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebd96561056f316ce129e9f7bdaa8043ee4c2f872903f36eff0920022487cba"},
- {file = "clickhouse_connect-0.6.13-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7de7099025a00954940dfe76f29ab529c5bece4ad8a2bc4131b7b8ff35ad60c8"},
+ {file = "clickhouse-connect-0.6.14.tar.gz", hash = "sha256:0531bbd5b8bdee616bf1cca5ddcb0af86db12e2b48fd39257a8ecdf32200bd57"},
+ {file = "clickhouse_connect-0.6.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:04affbd255fb8b1e4a882ddc1336c86530976d05578f47bb65e3a53471d291e4"},
+ {file = "clickhouse_connect-0.6.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f5bd61f2665f1890fa632b1181df2900ea838cf152cd9a3f775841ea2deab680"},
+ {file = "clickhouse_connect-0.6.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79897a0987008993f32737e17045a5c1982f9193f7511a3832a7ba3429cbf6b4"},
+ {file = "clickhouse_connect-0.6.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa95c8a96bdff593924407b074d616ee8a1bfb989579c17b330c6f3b27febfe3"},
+ {file = "clickhouse_connect-0.6.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501c0d843be30c86719b61089fb1de6298ac44b3670594f0a1cb0dc3ad97651e"},
+ {file = "clickhouse_connect-0.6.14-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1ec9672c9ed9d5e62f66ac14d6470b9b6be9946d6d24ddac87376437863b8f59"},
+ {file = "clickhouse_connect-0.6.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:92173354a6c7c5862fab09dab338197b86a192e0c117137e899e8cf92cc3b5b7"},
+ {file = "clickhouse_connect-0.6.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:757b4c05ebf10bdcb916334c3021ee571a61238907cdeee8c54bcf0550cd0d19"},
+ {file = "clickhouse_connect-0.6.14-cp310-cp310-win32.whl", hash = "sha256:2e74badf6c7569e1a0ad32f3be250a3ebf28a9df3b15c9709104e5f050486016"},
+ {file = "clickhouse_connect-0.6.14-cp310-cp310-win_amd64.whl", hash = "sha256:7b56c422467df5a0b2790e0943b747639f1f172fac7f8d9585adb3302c961fb1"},
+ {file = "clickhouse_connect-0.6.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d2aa6d28d79eb5ca94d7c756ec4dc599d2354897f5ef40fd0d8bdc579a81dd94"},
+ {file = "clickhouse_connect-0.6.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70cd5b2e0d80dc030355d09db213c73caa78ef259f2b04ce30c1c8cb513bf45b"},
+ {file = "clickhouse_connect-0.6.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:826c85e37555443af945a0d977598814ba7cb09447b0cdd167eae57dfd3f0724"},
+ {file = "clickhouse_connect-0.6.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cdb1f843d134a1e30828900bc51c9c1b4f4e638aac693767685e512fb095af5"},
+ {file = "clickhouse_connect-0.6.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a8ea6ca6e0d6b1af50078413e280f271559c462a8644541002e44c2cb5c371"},
+ {file = "clickhouse_connect-0.6.14-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8b72a5e5d54069dff419a6ec9bbc7f3896fe558551cae6a2b2cba60eaa0607a3"},
+ {file = "clickhouse_connect-0.6.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c531ed454ca9b6d85e739de3770a82eec2087ed2cb9660fb8ff0e62f7f1446cc"},
+ {file = "clickhouse_connect-0.6.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ae6ebf7c507f9d0fece9d1e387c9eec77762693f91647bca18f588cf1d594d24"},
+ {file = "clickhouse_connect-0.6.14-cp311-cp311-win32.whl", hash = "sha256:cf1e3067c2da8525b6f59a37f8e13cd6c4524f439be8fd7d8fa03f007f96c863"},
+ {file = "clickhouse_connect-0.6.14-cp311-cp311-win_amd64.whl", hash = "sha256:15a040210877cc34155943c7870bf78247d4d4fa3bd4e0595ca22e97760679b7"},
+ {file = "clickhouse_connect-0.6.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eb91e4ee0435088fc8bd36de51a93ff9286a514d82ac373b57b2d6cad4655d77"},
+ {file = "clickhouse_connect-0.6.14-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48108bb1cfe99b6ff60344838859aec1315213dfa618f6ca4b92c0c6e5ae8d41"},
+ {file = "clickhouse_connect-0.6.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c75d4bd8ef0b90f9e89ea70c16ff099278e4bb8f1e045008376ac34c6122b73d"},
+ {file = "clickhouse_connect-0.6.14-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:897f40eda84e9c45d0bdaf3a9e638e614e236a4a5eeab5cddd920857f9f8f22a"},
+ {file = "clickhouse_connect-0.6.14-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5ecc88656df05ae49e70062aee7022982eec3f87fb14db97c25276fef6633d7c"},
+ {file = "clickhouse_connect-0.6.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:76cec48265774ae3fa61a77b290dcc8385aad4312a8d7dfcaffb9fc00f79458e"},
+ {file = "clickhouse_connect-0.6.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dba280e00ec4cfe0e4d69f88baa9a0491bc1ed83ec57336e5197adae8d42d0c9"},
+ {file = "clickhouse_connect-0.6.14-cp37-cp37m-win32.whl", hash = "sha256:6c77f537e04747702e009c05f4a7f6f96cbe1696bb89d29f72e39e7370924836"},
+ {file = "clickhouse_connect-0.6.14-cp37-cp37m-win_amd64.whl", hash = "sha256:d0eceaff68a53f71384bb9aee7fc1630f68ac10538727c8516ae0af1103f2580"},
+ {file = "clickhouse_connect-0.6.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dfa09948caeed539cdd019a1e341a379a1dcacdd755b278d12484b4a703afa3"},
+ {file = "clickhouse_connect-0.6.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a30d99cb1fd57b8fed4449632e51d48386d0eec1673f905572c5fc7059215c20"},
+ {file = "clickhouse_connect-0.6.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e88de4fe66ae2b1c15726760cc87a703e4d1162de52a19c8d8b57a4429f08e"},
+ {file = "clickhouse_connect-0.6.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03d721de610beae823068665d6c604a5f390a99e7b2354264b17136a3a520b13"},
+ {file = "clickhouse_connect-0.6.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a627762f2f692e226b3cb574a83133844213c6507c6313d3fefd8a3de08e5798"},
+ {file = "clickhouse_connect-0.6.14-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:62a596f8d9db8592477a2032c329be7449ea32d133cdc4e5d6f804e251b8617a"},
+ {file = "clickhouse_connect-0.6.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e8ab9e5a61968c328a0fdc254b02b96142ebb4ec2bc1623f9498538f0ebfdc7c"},
+ {file = "clickhouse_connect-0.6.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6c21fe379b1b8822eb9644600e38220b5c4b530fd0f2b1da824a0918120a8f01"},
+ {file = "clickhouse_connect-0.6.14-cp38-cp38-win32.whl", hash = "sha256:2a17b336781d3fbb67ed556918c17e63c7d462709aa6a953bb3410ddb67fd7f4"},
+ {file = "clickhouse_connect-0.6.14-cp38-cp38-win_amd64.whl", hash = "sha256:838a008c0f7d911ab81f741ea27a64ef7bdcc2508698b70f018987dfc742ffa9"},
+ {file = "clickhouse_connect-0.6.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:083649a97c3f366f66f0f2578b9f88d86c1d3a40b9015c9403db524fda36a952"},
+ {file = "clickhouse_connect-0.6.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e9bd6849852b2c55e51a477e10bc8b61990c5f37f31cce5ea6fc970b447b5af"},
+ {file = "clickhouse_connect-0.6.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9152c45423f488cf6229bce1f9e695cd81e7ffcd3ae0f1e40e5e62079b18d4a5"},
+ {file = "clickhouse_connect-0.6.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341e068d4a6a423ed22fb3b96cfe16be0d6305943c3fb1cc48251b7d9729931d"},
+ {file = "clickhouse_connect-0.6.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ead7acb841524bd7a73b1f10592a36e901d63bc89af3270ab76b89a11d44fe20"},
+ {file = "clickhouse_connect-0.6.14-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8bce432f72dcf6679c2d0bac4e3a82a126389ad7951d316f213109cee6925c7c"},
+ {file = "clickhouse_connect-0.6.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1f403499f169574cafb05888dfdaf18065cc49ff1321e5e108c504c8c220e172"},
+ {file = "clickhouse_connect-0.6.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3189fcd339bfd7ae4e703ff40b110b9740d6b1ec8385ed8bd1547663fd046578"},
+ {file = "clickhouse_connect-0.6.14-cp39-cp39-win32.whl", hash = "sha256:a30de3f0997a9157e840c2d4e07fd9c6fc6e359f1ff9f3a46386b5abdca73c1a"},
+ {file = "clickhouse_connect-0.6.14-cp39-cp39-win_amd64.whl", hash = "sha256:c3476a95780374e94dfba2a28093d15f8370bfa6f4cb46a02e0af8813e5f7368"},
+ {file = "clickhouse_connect-0.6.14-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:22affe46983e67e3923e9330336d21e9ec4b4812b6fbeb1865514145b3870170"},
+ {file = "clickhouse_connect-0.6.14-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62727090af8875631115911f58442967386b31cd4efa93c951c2aa7e57d1ce4b"},
+ {file = "clickhouse_connect-0.6.14-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee4ea5ac58de0580f2e12b46cfd2f8d13c1e690378bf9775bfed0c935232de71"},
+ {file = "clickhouse_connect-0.6.14-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a126fe486dd02fa5f8adb0b9d8fd0fc701fb73b2275e1040ed210afadd189f90"},
+ {file = "clickhouse_connect-0.6.14-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:de6bf773c8776033ca5fb5a6a376729ae69afdd0b19a71d1460d1a221fc5a627"},
+ {file = "clickhouse_connect-0.6.14-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d27d2c9698d1acb550ac8c30c4d9440c7d826a16444e4aea4dacf11ed7ec8988"},
+ {file = "clickhouse_connect-0.6.14-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f57efbe536dfbfb7e10dd16ced6fe02441fb174450760f0b29b2b60d23c6462f"},
+ {file = "clickhouse_connect-0.6.14-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c134483da38a3d3e38c44da9f3d519d73e177998052d36129e21863c7a3497ee"},
+ {file = "clickhouse_connect-0.6.14-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d6ae7ccb4ca3d310c2971ead9839935890e40da8602dcc92ecda9bbbb24366"},
+ {file = "clickhouse_connect-0.6.14-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0acf6b69b11b757d60545b0ccac3df4980f69351994e30074df84729bb5af5d1"},
+ {file = "clickhouse_connect-0.6.14-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e043b3b606749d23eca7601a1a44f188c6f117ae57a2852c66c21f11b7296fe4"},
+ {file = "clickhouse_connect-0.6.14-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a887dfef3f3914454c7d7a428db8063b1678c66678cbabcd6368f0b67876f1"},
+ {file = "clickhouse_connect-0.6.14-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e255e7c9c38fb9bceefc659374d04914ef2222a6f121fccf86a865b81110e96b"},
+ {file = "clickhouse_connect-0.6.14-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2be9a6ba1d3055bb6956be218ffecfa3bfbe47121dfa34467815aa883f15d159"},
+ {file = "clickhouse_connect-0.6.14-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:59faa034fdd58c1e7c8b2f4a033e9c611a0c58e193339cdd62d9d91a62f11195"},
]
[package.dependencies]
@@ -826,6 +1059,21 @@ lint = ["black (>=22.6.0)", "mdformat (>0.7)", "mdformat-gfm (>=0.3.5)", "ruff (
test = ["pytest"]
typing = ["mypy (>=0.990)"]
+[[package]]
+name = "configargparse"
+version = "1.7"
+description = "A drop-in replacement for argparse that allows options to also be set via config files and/or environment variables."
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "ConfigArgParse-1.7-py3-none-any.whl", hash = "sha256:d249da6591465c6c26df64a9f73d2536e743be2f244eb3ebe61114af2f94f86b"},
+ {file = "ConfigArgParse-1.7.tar.gz", hash = "sha256:e7067471884de5478c58a511e529f0f9bd1c66bfef1dea90935438d6c23306d1"},
+]
+
+[package.extras]
+test = ["PyYAML", "mock", "pytest"]
+yaml = ["PyYAML"]
+
[[package]]
name = "coverage"
version = "7.3.1"
@@ -1448,6 +1696,56 @@ files = [
{file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"},
]
+[[package]]
+name = "flask"
+version = "2.3.3"
+description = "A simple framework for building complex web applications."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b"},
+ {file = "flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc"},
+]
+
+[package.dependencies]
+blinker = ">=1.6.2"
+click = ">=8.1.3"
+importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""}
+itsdangerous = ">=2.1.2"
+Jinja2 = ">=3.1.2"
+Werkzeug = ">=2.3.7"
+
+[package.extras]
+async = ["asgiref (>=3.2)"]
+dotenv = ["python-dotenv"]
+
+[[package]]
+name = "flask-basicauth"
+version = "0.2.0"
+description = "HTTP basic access authentication for Flask."
+optional = false
+python-versions = "*"
+files = [
+ {file = "Flask-BasicAuth-0.2.0.tar.gz", hash = "sha256:df5ebd489dc0914c224419da059d991eb72988a01cdd4b956d52932ce7d501ff"},
+]
+
+[package.dependencies]
+Flask = "*"
+
+[[package]]
+name = "flask-cors"
+version = "4.0.0"
+description = "A Flask extension adding a decorator for CORS support"
+optional = false
+python-versions = "*"
+files = [
+ {file = "Flask-Cors-4.0.0.tar.gz", hash = "sha256:f268522fcb2f73e2ecdde1ef45e2fd5c71cc48fe03cffb4b441c6d1b40684eb0"},
+ {file = "Flask_Cors-4.0.0-py2.py3-none-any.whl", hash = "sha256:bc3492bfd6368d27cfe79c7821df5a8a319e1a6d5eab277a3794be19bdc51783"},
+]
+
+[package.dependencies]
+Flask = ">=0.9"
+
[[package]]
name = "flatbuffers"
version = "23.5.26"
@@ -1459,6 +1757,24 @@ files = [
{file = "flatbuffers-23.5.26.tar.gz", hash = "sha256:9ea1144cac05ce5d86e2859f431c6cd5e66cd9c78c558317c7955fb8d4c78d89"},
]
+[[package]]
+name = "flower"
+version = "2.0.1"
+description = "Celery Flower"
+optional = true
+python-versions = ">=3.7"
+files = [
+ {file = "flower-2.0.1-py2.py3-none-any.whl", hash = "sha256:9db2c621eeefbc844c8dd88be64aef61e84e2deb29b271e02ab2b5b9f01068e2"},
+ {file = "flower-2.0.1.tar.gz", hash = "sha256:5ab717b979530770c16afb48b50d2a98d23c3e9fe39851dcf6bc4d01845a02a0"},
+]
+
+[package.dependencies]
+celery = ">=5.0.5"
+humanize = "*"
+prometheus-client = ">=0.8.0"
+pytz = "*"
+tornado = ">=5.0.0,<7.0.0"
+
[[package]]
name = "frozenlist"
version = "1.4.0"
@@ -1564,6 +1880,191 @@ smb = ["smbprotocol"]
ssh = ["paramiko"]
tqdm = ["tqdm"]
+[[package]]
+name = "gevent"
+version = "23.9.1"
+description = "Coroutine-based network library"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "gevent-23.9.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771"},
+ {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69"},
+ {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535"},
+ {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a"},
+ {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39"},
+ {file = "gevent-23.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae"},
+ {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6"},
+ {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7"},
+ {file = "gevent-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e"},
+ {file = "gevent-23.9.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c"},
+ {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648"},
+ {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07"},
+ {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9"},
+ {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011"},
+ {file = "gevent-23.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7"},
+ {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71"},
+ {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e"},
+ {file = "gevent-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a"},
+ {file = "gevent-23.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904"},
+ {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b"},
+ {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e"},
+ {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599"},
+ {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea"},
+ {file = "gevent-23.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599"},
+ {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303"},
+ {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d"},
+ {file = "gevent-23.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1"},
+ {file = "gevent-23.9.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe"},
+ {file = "gevent-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5"},
+ {file = "gevent-23.9.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397"},
+ {file = "gevent-23.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507"},
+ {file = "gevent-23.9.1-cp38-cp38-win32.whl", hash = "sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a"},
+ {file = "gevent-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f"},
+ {file = "gevent-23.9.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a"},
+ {file = "gevent-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653"},
+ {file = "gevent-23.9.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd"},
+ {file = "gevent-23.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543"},
+ {file = "gevent-23.9.1-cp39-cp39-win32.whl", hash = "sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2"},
+ {file = "gevent-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b"},
+ {file = "gevent-23.9.1.tar.gz", hash = "sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34"},
+]
+
+[package.dependencies]
+cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
+greenlet = {version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""}
+"zope.event" = "*"
+"zope.interface" = "*"
+
+[package.extras]
+dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"]
+docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"]
+monitor = ["psutil (>=5.7.0)"]
+recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"]
+test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests", "setuptools"]
+
+[[package]]
+name = "geventhttpclient"
+version = "2.0.10"
+description = "http client library for gevent"
+optional = false
+python-versions = "*"
+files = [
+ {file = "geventhttpclient-2.0.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2ba6814f4a31286573f7fd24154bdb9cbe4ae01e754f48d71b1944798bf663"},
+ {file = "geventhttpclient-2.0.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01dfb0de68f219b7a534121caa71481e32574bba7fe547fa37ee47a73a7b224"},
+ {file = "geventhttpclient-2.0.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1b2c7e6bb15910a2e86f8da375adfd63ac07587a1c764cedc082b00390bcd76e"},
+ {file = "geventhttpclient-2.0.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:022801e2323e3e673e3c7034f6bc5440b4651649df03396eb1b3a86a6aba899d"},
+ {file = "geventhttpclient-2.0.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcfc1489e71b010d8ce8857578cdb1b8ba348626807aa9d077fc73c9864e51e1"},
+ {file = "geventhttpclient-2.0.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef527f67653488000218283368e526fa699604e03e98ce4e0e588e89116977d"},
+ {file = "geventhttpclient-2.0.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a673a4b6b1839c8491372e43208912040c25a34254c60bf1d084489ddc300ee"},
+ {file = "geventhttpclient-2.0.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2ceae04de8bdb4ef1d0ca7724cd9cad77c6611aac3830a24a7f13747e8b748c7"},
+ {file = "geventhttpclient-2.0.10-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:dc3effc56065a5c26292ca26127e6fdd0f68429b413e847a8b7bad38972aab53"},
+ {file = "geventhttpclient-2.0.10-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:304c1d24d33b19cae53614ffc91c68d1e682d8b60a4d9eefcf87fcd099b1c2f2"},
+ {file = "geventhttpclient-2.0.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d39b468aba4dbec358eb0205d41791afc53651eee789566239e544ed6c8b7dbb"},
+ {file = "geventhttpclient-2.0.10-cp310-cp310-win32.whl", hash = "sha256:91cd3f680ee413cc83819f0e1f63e49297c550099e85bbee92e73960d3eba041"},
+ {file = "geventhttpclient-2.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:9a9c02c44b1e4e6edf929aa7c98b665f4db9cdcd406e4a9b4897f48127a6dd6b"},
+ {file = "geventhttpclient-2.0.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cb079f45fdc8e2bf7157ef55727d8c2bb7c95fb4f754dac61d7a9b91da0c5c1a"},
+ {file = "geventhttpclient-2.0.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd657ba277f4888b5a4b5da72a587641d6725d1e6ab0dd6875f29ad0a3458ad5"},
+ {file = "geventhttpclient-2.0.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fd589226e908a6c2556572ff3b13fe00308849b44dec47bb794de27afa0339de"},
+ {file = "geventhttpclient-2.0.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e38a4123ed63935ccaf18054135e50fe4f798744f10d37faa9d2eaddfcff12f"},
+ {file = "geventhttpclient-2.0.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9711c629559b1f0be4977a2be79899fb90085a5a1f85ca435ec91d6a5648ff3f"},
+ {file = "geventhttpclient-2.0.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7435458eada516d1caf8499a2618db1160e62bbe0c8e4d6f3ab03fc507587dff"},
+ {file = "geventhttpclient-2.0.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:446b17f342315d8c63c020732b9ab939a874014c84cf250d66ffd96c877f6d96"},
+ {file = "geventhttpclient-2.0.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:68279ab30e20f48fbac4371cd5850c77ecc37f24ef656f8c37afd5454576bc57"},
+ {file = "geventhttpclient-2.0.10-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0823827d029aed708d6ed577994cdd3b4c1466690be0b7f27f3b459783ab7c6a"},
+ {file = "geventhttpclient-2.0.10-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:505722ef30d680c983812795e047dbb2d09dc0f476015e5d3411251bb960e3b1"},
+ {file = "geventhttpclient-2.0.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8ebf2ce3ca1308ffc9daae1f45860b2f5a3c1a75f6c46b2d12b9a478f7c4f05e"},
+ {file = "geventhttpclient-2.0.10-cp311-cp311-win32.whl", hash = "sha256:4ad20f6f03e34e44366e6794a28bd0b35b09e1dca3350bbf0a72c50d64c9c266"},
+ {file = "geventhttpclient-2.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:c1883adee9c69374e30f43f7bea85dd4a7b4cc440e3c6ecf975bef04e1f8a972"},
+ {file = "geventhttpclient-2.0.10-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7525bd48fadc93a79d13383cf38a10eed6c9f2f3c65e1f3a8cd4978dfcf023a0"},
+ {file = "geventhttpclient-2.0.10-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c6820591122c4444652238806c0c97f6c0de76d790bab255fd242962c8026654"},
+ {file = "geventhttpclient-2.0.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34a06879a2dbc2a78edf8cfcabbcc67a177d0642b0e4490b771b72ebceea4537"},
+ {file = "geventhttpclient-2.0.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:441a16eb8743b44c7b6f0acbbdc38d6f407f0763eb859ae0ae6e61427ac86c3e"},
+ {file = "geventhttpclient-2.0.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf3a7a5c6b244c6d55de9d28245f70ee33cca8353355a9b10ea0c2e08ff24a0"},
+ {file = "geventhttpclient-2.0.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e65a91c7a4e559d0f60dab4439d15355ade9c30f5c73276bb748b000a062e8f"},
+ {file = "geventhttpclient-2.0.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d051f15c3c92140ce142336ae05a76868ce05a86b4e15c5becb5431beaf53a07"},
+ {file = "geventhttpclient-2.0.10-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:224d1f34e32d683889f8a92f92ce3a1e4bb2f3f4a3d85b931a8df493dd59e9e7"},
+ {file = "geventhttpclient-2.0.10-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:309a35f033766476765a48a9c2712ffb988c3e3d50cd4b98eaa77e34b470af7e"},
+ {file = "geventhttpclient-2.0.10-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:d9d09579969cfb244e88bb599ac93549d8c0b56018de1f1ffade4a986951ad1d"},
+ {file = "geventhttpclient-2.0.10-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:15ecc1599646755d7e2e5648606c21ace00c3337c2d72d33b4e2de5d66b4ed65"},
+ {file = "geventhttpclient-2.0.10-cp312-cp312-win32.whl", hash = "sha256:c99dd907622f28523c3f90b8718643c103ce6519be7128e75730c398fd23d157"},
+ {file = "geventhttpclient-2.0.10-cp312-cp312-win_amd64.whl", hash = "sha256:9532ee9066072737fe0eac1714c99792d7007769d529a056bc0c238946f67fdf"},
+ {file = "geventhttpclient-2.0.10-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7197223e650b9e07e1b3ddc1b41b8cdc1c2c4b408f392bdf827efa8c0cb6c67b"},
+ {file = "geventhttpclient-2.0.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304b9d67c91db96633d89b938b68020d7f787ff962580b1cff819d4218d7eb45"},
+ {file = "geventhttpclient-2.0.10-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdd4aa396f69f4063b7fcddb2c400c9eea933bcce63f3c65fc28a1869c88179c"},
+ {file = "geventhttpclient-2.0.10-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8da6f49e65cdcc19fbc37978d9e3e14ba30c949d9a5492e0f96e1e5270f2305"},
+ {file = "geventhttpclient-2.0.10-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba65c48a63042deadc8e4f1f5348409d851d6fa06f8a1b5a58fd6fd24e50daaf"},
+ {file = "geventhttpclient-2.0.10-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:f3dcbf4a53852498128937e870c4b0ced1ed49b9153c68d12a52a0711652b9cf"},
+ {file = "geventhttpclient-2.0.10-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:976e86d63fd1cd3bda4f78ec469b6d1c8dec4259abeb62191464e8dd4d40bb8e"},
+ {file = "geventhttpclient-2.0.10-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:05be35bc7d2bd3ad6f0aa5042ae5a0b171ff19ec75ffeae1b4a2698572dd67a4"},
+ {file = "geventhttpclient-2.0.10-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b3556a97e1c71d6b323076e6153f3afcf4f2e37ad79c3fe6d5bf006d3c1b5436"},
+ {file = "geventhttpclient-2.0.10-cp36-cp36m-win32.whl", hash = "sha256:2b2d801205000f673f879b4edc1064b7dfc1bdd0dc5257bf565e6e7386b818bf"},
+ {file = "geventhttpclient-2.0.10-cp36-cp36m-win_amd64.whl", hash = "sha256:7fd672aa9186607ac802b09efd3c835eb808f930a5c3489553dbfcbe78539129"},
+ {file = "geventhttpclient-2.0.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:551b147e4c2ea60bfefc4f12dd061bfe84792497a32a369f8dab7f503da5aa05"},
+ {file = "geventhttpclient-2.0.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:113c001d339b9e5c209f6f9da9271b0011341c25a4171d142c1d802bc0889ec4"},
+ {file = "geventhttpclient-2.0.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fcdfdbbbf3c96265aca110433a5ce68e807336fa58bd7ef0651e66032037575"},
+ {file = "geventhttpclient-2.0.10-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:223daf689883680eef2aa1b773a2bd7e6500749615332b0a0949ee11afeeeff9"},
+ {file = "geventhttpclient-2.0.10-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a45343be209224de6e525611938a41a4269c36df3b3c1f6e12f99af188d192a4"},
+ {file = "geventhttpclient-2.0.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c268f7573f2b3cceabdc086abca96a59fb2766acbf60fb349ccbc208b6051e7c"},
+ {file = "geventhttpclient-2.0.10-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c8117ef8e405fa05f5ea50fd9eb1d538bb7eeb86dba2849fb25d8296fabb70fc"},
+ {file = "geventhttpclient-2.0.10-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fb70548781b3ba3531ec3572ae6f4cd87f387822c412fff1ee6fe52c2e4b66cf"},
+ {file = "geventhttpclient-2.0.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b87dcef1ca6eb9127fd60844f1dd77063081335079b498bc1e1cd8e4764b6863"},
+ {file = "geventhttpclient-2.0.10-cp37-cp37m-win32.whl", hash = "sha256:8ff70f24183705f2cb63dc132b4dd4e0eec58b8f182fde76f5a205e4608266cd"},
+ {file = "geventhttpclient-2.0.10-cp37-cp37m-win_amd64.whl", hash = "sha256:f30d83152339779650a97471f27ef2fb2e6804ce660c96790c0d01c66648483f"},
+ {file = "geventhttpclient-2.0.10-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:786e120946a4dc1c7ede5a04943119540a1ccc22227029cdb7988a3d216885b1"},
+ {file = "geventhttpclient-2.0.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c27943dff539c4bc19f31ea8bffbb79a215e3b3f72b87686791956af39586ac4"},
+ {file = "geventhttpclient-2.0.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a377934706416ef04b92291540b609c7dde004a7ccb3b4e047873af2432d78e4"},
+ {file = "geventhttpclient-2.0.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b91c1a73e99ef4542e7098a997d1a4bce08cafb935e24a7b6d02c6da2359c91d"},
+ {file = "geventhttpclient-2.0.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80683052062f4cb6f18e7d2b1dba91f9536f7955a12660d599ed64bb4aa56d1e"},
+ {file = "geventhttpclient-2.0.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44c06c9882cc50201e5c7fe667eae73a491b6590018aa43c54c79e88c30abdb0"},
+ {file = "geventhttpclient-2.0.10-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271834908b41954fedc777478ffdc10050387bb5863805e19301b80e0fd85618"},
+ {file = "geventhttpclient-2.0.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f2f48dfd37d341c8433e7a2f76108b3d21614ccf2fbe00051d9dd29b3258efa6"},
+ {file = "geventhttpclient-2.0.10-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d6110eb2f685c6dcaff56e9b3b161da2eb432eea15b68cee0f51ec5d28c886ea"},
+ {file = "geventhttpclient-2.0.10-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e9c1c080a527dd9047e0421231cdd2394eeb92f94836f4ad7d7fece937ba26"},
+ {file = "geventhttpclient-2.0.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fa34492682347420384e49bd7c15886330af685924fc64badefce642867e4856"},
+ {file = "geventhttpclient-2.0.10-cp38-cp38-win32.whl", hash = "sha256:ea66408103b4c8954cbd3cc464be0e968a139d073987555a280760fb56fed41f"},
+ {file = "geventhttpclient-2.0.10-cp38-cp38-win_amd64.whl", hash = "sha256:ef45b4facbaf7793373a32cab3f3e9460b76eb5f854b066f53b4753eca0efa7d"},
+ {file = "geventhttpclient-2.0.10-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75e09f122824d1d4aa3e9e48089a5e6f5c0925c817dfb99a65aeafa173243a27"},
+ {file = "geventhttpclient-2.0.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c9b4269873ce14bdd0977ae7b3b29e55ba2dc187b1088665cfe78fc094fc6795"},
+ {file = "geventhttpclient-2.0.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39db24bf426471cf81530675459ea209355032bf73f55a6e111f28853fe7564f"},
+ {file = "geventhttpclient-2.0.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf9148ce0072e6f2c644522b38d22d2749367dd599a4b32991ca9fc5feb112c5"},
+ {file = "geventhttpclient-2.0.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:452624f7e1b16e27c5df5b4f639a5a45f9372d9d9a8e7d2754f2f034b04d43d3"},
+ {file = "geventhttpclient-2.0.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57ecf4d4a09b502d2d4b25bc074b10290d595d80b6ce86294ecdd992cff80fb9"},
+ {file = "geventhttpclient-2.0.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e338a3b23d05c7550a6163b8d367de953be25f1d94949d043b325390df72d527"},
+ {file = "geventhttpclient-2.0.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6d2b0b87bb933911dadb702df74a7da91a4cdd86b6d75800db8494d7e5709e70"},
+ {file = "geventhttpclient-2.0.10-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5396759194bef95b92128dfd8b80d332f809de23193a2529071a519afd6e9250"},
+ {file = "geventhttpclient-2.0.10-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c13ba845f042d0807f6024b80c458b22b615028cc4f4ad23bd67c6db9de9969e"},
+ {file = "geventhttpclient-2.0.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24b33b88fc7b815139b80736bb46a7f6113abc7edd3d846cd99fc8d8c31d8439"},
+ {file = "geventhttpclient-2.0.10-cp39-cp39-win32.whl", hash = "sha256:e545fa59f298f6fc5219403f06819271f77a67216d1352f5cf703f292be05c3e"},
+ {file = "geventhttpclient-2.0.10-cp39-cp39-win_amd64.whl", hash = "sha256:216e451ea72fe2372bc72e34f214ef8bc9d62b049d9048f88f81338e1c6008a5"},
+ {file = "geventhttpclient-2.0.10-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:71a224032b2da3fe30d75fb57fb9d3e8ff323895e14facd9374e585d5bf52d01"},
+ {file = "geventhttpclient-2.0.10-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bed5b6051582bdf39c7ff15051ec0758d1a0ebcb6ff09b6ae600717caf3f379e"},
+ {file = "geventhttpclient-2.0.10-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e2e2eed1d821854f9f32f475d258af605a87ce12dc4d93abe61c022bf2bb06e"},
+ {file = "geventhttpclient-2.0.10-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2cd1ecc9a389ca54a5e538769c3f35a82478006dc50eb505989d2ff6c3cf518"},
+ {file = "geventhttpclient-2.0.10-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fb7018bfd4f6808d4e3b9cdda2dcb52cc01236a4bb65e18021fb8816996e9cd3"},
+ {file = "geventhttpclient-2.0.10-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6df4957678eb05a6ccfbbb96a9490345758620b53fe5653052979df94819765b"},
+ {file = "geventhttpclient-2.0.10-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:508bcc82f7259e316f2060025e7ff899acc8032c645e765bb780083e39060c07"},
+ {file = "geventhttpclient-2.0.10-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:961dac20f0f4b8cfa4e2eaafe2d20d74448a5a04239135fa1867a9a1bc3fd986"},
+ {file = "geventhttpclient-2.0.10-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:826fa6487ba1c1b7186dcdc300c216fd9b8cf34e307335b4cad1769736988ce9"},
+ {file = "geventhttpclient-2.0.10-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:b30c9495b43b2f67b6a0102ee3fd497762b6bf972e435e5974fd8d2cb5263235"},
+ {file = "geventhttpclient-2.0.10-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e2a9a5e52abc8d3275902c1199ff810264b033e346bcf19da920f9b6de1ea916"},
+ {file = "geventhttpclient-2.0.10-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:993b653c7c3d4e0683067b2e324fd749649e87b453205def6a4809dd30498b44"},
+ {file = "geventhttpclient-2.0.10-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6aa110a66936c37a8bbc2a51515fc0f7e99404e245ef15af8346fa2f6b4f6698"},
+ {file = "geventhttpclient-2.0.10-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ce4699fcadc17786cba0b461ff59d0231277155439b274fa12861f93fa764c8"},
+ {file = "geventhttpclient-2.0.10-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:23fedc0bf219cc42f9b3d565953a08a429c09642859b86174535977bb281f1c1"},
+ {file = "geventhttpclient-2.0.10-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a9e99fa203a43a8393cf01e5a6433e74524c97bf67e39c4f062c8fff27f49360"},
+ {file = "geventhttpclient-2.0.10-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dfcfdc3a05e2145fe15f8b9cc7dd8b9fcd545dd42d051be0a2a828b8b3cc4a3"},
+ {file = "geventhttpclient-2.0.10-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b56abccfea3fe154f29dbc518737c70d63c1f44da4c956e31e9a5ff65074de19"},
+ {file = "geventhttpclient-2.0.10-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9f8d335e7e3579608904e001ba9b055d3f30b184db550c32901b3b86af71b92"},
+ {file = "geventhttpclient-2.0.10-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:96f1a63f18eb4a66ea9d6ab211d756afe8b6d5c6e80beff298cd7b65a3d710c7"},
+ {file = "geventhttpclient-2.0.10.tar.gz", hash = "sha256:b7c97b26511957a36a894ec54651c08890a69e118b69755f8e74bfc37c63391b"},
+]
+
+[package.dependencies]
+brotli = "*"
+certifi = "*"
+gevent = ">=0.13"
+six = "*"
+
[[package]]
name = "gitdb"
version = "4.0.10"
@@ -2427,6 +2928,20 @@ files = [
[package.dependencies]
pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""}
+[[package]]
+name = "humanize"
+version = "4.8.0"
+description = "Python humanize utilities"
+optional = true
+python-versions = ">=3.8"
+files = [
+ {file = "humanize-4.8.0-py3-none-any.whl", hash = "sha256:8bc9e2bb9315e61ec06bf690151ae35aeb65651ab091266941edf97c90836404"},
+ {file = "humanize-4.8.0.tar.gz", hash = "sha256:9783373bf1eec713a770ecaa7c2d7a7902c98398009dfa3d8a2df91eec9311e8"},
+]
+
+[package.extras]
+tests = ["freezegun", "pytest", "pytest-cov"]
+
[[package]]
name = "hyperframe"
version = "6.0.1"
@@ -2581,6 +3096,17 @@ qtconsole = ["qtconsole"]
test = ["pytest (<7.1)", "pytest-asyncio", "testpath"]
test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"]
+[[package]]
+name = "itsdangerous"
+version = "2.1.2"
+description = "Safely pass data to untrusted environments and back."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
+ {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
+]
+
[[package]]
name = "jaraco-classes"
version = "3.3.0"
@@ -2801,7 +3327,7 @@ full = ["aiohttp", "black (==22.3.0)", "docker", "filelock", "flake8 (==4.0.1)",
name = "jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
-optional = true
+optional = false
python-versions = ">=3.7"
files = [
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
@@ -2891,6 +3417,39 @@ completion = ["shtab"]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
+[[package]]
+name = "kombu"
+version = "5.3.2"
+description = "Messaging library for Python."
+optional = true
+python-versions = ">=3.8"
+files = [
+ {file = "kombu-5.3.2-py3-none-any.whl", hash = "sha256:b753c9cfc9b1e976e637a7cbc1a65d446a22e45546cd996ea28f932082b7dc9e"},
+ {file = "kombu-5.3.2.tar.gz", hash = "sha256:0ba213f630a2cb2772728aef56ac6883dc3a2f13435e10048f6e97d48506dbbd"},
+]
+
+[package.dependencies]
+amqp = ">=5.1.1,<6.0.0"
+typing-extensions = {version = "*", markers = "python_version < \"3.10\""}
+vine = "*"
+
+[package.extras]
+azureservicebus = ["azure-servicebus (>=7.10.0)"]
+azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"]
+confluentkafka = ["confluent-kafka (==2.1.1)"]
+consul = ["python-consul2"]
+librabbitmq = ["librabbitmq (>=2.0.0)"]
+mongodb = ["pymongo (>=4.1.1)"]
+msgpack = ["msgpack"]
+pyro = ["pyro4"]
+qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"]
+redis = ["redis (>=4.5.2)"]
+slmq = ["softlayer-messaging (>=1.0.3)"]
+sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"]
+sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"]
+yaml = ["PyYAML (>=3.10)"]
+zookeeper = ["kazoo (>=2.8.0)"]
+
[[package]]
name = "langchain"
version = "0.0.274"
@@ -3041,6 +3600,33 @@ typing-extensions = ">=4.5.0"
[package.extras]
server = ["fastapi (>=0.100.0)", "pydantic-settings (>=2.0.1)", "sse-starlette (>=1.6.1)", "uvicorn (>=0.22.0)"]
+[[package]]
+name = "locust"
+version = "2.16.1"
+description = "Developer friendly load testing framework"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "locust-2.16.1-py3-none-any.whl", hash = "sha256:d0f01f9fca6a7d9be987b32185799d9e219fce3b9a3b8250ea03e88003335804"},
+ {file = "locust-2.16.1.tar.gz", hash = "sha256:cd54f179b679ae927e9b3ffd2b6a7c89c1078103cfbe96b4dd53c7872774b619"},
+]
+
+[package.dependencies]
+ConfigArgParse = ">=1.0"
+flask = ">=2.0.0"
+Flask-BasicAuth = ">=0.2.0"
+Flask-Cors = ">=3.0.10"
+gevent = ">=20.12.1"
+geventhttpclient = ">=2.0.2"
+msgpack = ">=0.6.2"
+psutil = ">=5.6.7"
+pywin32 = {version = "*", markers = "platform_system == \"Windows\""}
+pyzmq = ">=22.2.1,<23.0.0 || >23.0.0"
+requests = ">=2.23.0"
+roundrobin = ">=0.0.2"
+typing-extensions = ">=3.7.4.3"
+Werkzeug = ">=2.0.0"
+
[[package]]
name = "loguru"
version = "0.7.2"
@@ -3374,13 +3960,13 @@ files = [
[[package]]
name = "metal-sdk"
-version = "2.1.2"
+version = "2.1.3"
description = "SDK for getmetal.io"
optional = false
python-versions = ">=3.7"
files = [
- {file = "metal_sdk-2.1.2-py3-none-any.whl", hash = "sha256:542ee620173994b43344779d8d9fc5540b06d4ac30941d824769f1e8bf767c0c"},
- {file = "metal_sdk-2.1.2.tar.gz", hash = "sha256:a22895a5babd9c6bdd651ca4289f593ba5eee9742b97d12e6c5a859a3befef96"},
+ {file = "metal_sdk-2.1.3-py3-none-any.whl", hash = "sha256:c79f4ddb78b9ec83bb73969ce535ae33c601d34525088ee40268bc2ac5f69ba2"},
+ {file = "metal_sdk-2.1.3.tar.gz", hash = "sha256:43cb6244d7af5e1fe21b3d0fe59ee2e0f40d0350ea4779e80d9cf366d760407e"},
]
[package.dependencies]
@@ -3440,6 +4026,71 @@ docs = ["sphinx"]
gmpy = ["gmpy2 (>=2.1.0a4)"]
tests = ["pytest (>=4.6)"]
+[[package]]
+name = "msgpack"
+version = "1.0.6"
+description = "MessagePack serializer"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "msgpack-1.0.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f4321692e7f299277e55f322329b2c972d93bb612d85f3fda8741bec5c6285ce"},
+ {file = "msgpack-1.0.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f0e36a5fa7a182cde391a128a64f437657d2b9371dfa42eda3436245adccbf5"},
+ {file = "msgpack-1.0.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b5c8dd9a386a66e50bd7fa22b7a49fb8ead2b3574d6bd69eb1caced6caea0803"},
+ {file = "msgpack-1.0.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f85200ea102276afdd3749ca94747f057bbb868d1c52921ee2446730b508d0f"},
+ {file = "msgpack-1.0.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a006c300e82402c0c8f1ded11352a3ba2a61b87e7abb3054c845af2ca8d553c"},
+ {file = "msgpack-1.0.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33bbf47ea5a6ff20c23426106e81863cdbb5402de1825493026ce615039cc99d"},
+ {file = "msgpack-1.0.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:04450e4b5e1e662e7c86b6aafb7c230af9334fd0becf5e6b80459a507884241c"},
+ {file = "msgpack-1.0.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b06a5095a79384760625b5de3f83f40b3053a385fb893be8a106fbbd84c14980"},
+ {file = "msgpack-1.0.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3910211b0ab20be3a38e0bb944ed45bd4265d8d9f11a3d1674b95b298e08dd5c"},
+ {file = "msgpack-1.0.6-cp310-cp310-win32.whl", hash = "sha256:1dc67b40fe81217b308ab12651adba05e7300b3a2ccf84d6b35a878e308dd8d4"},
+ {file = "msgpack-1.0.6-cp310-cp310-win_amd64.whl", hash = "sha256:885de1ed5ea01c1bfe0a34c901152a264c3c1f8f1d382042b92ea354bd14bb0e"},
+ {file = "msgpack-1.0.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:099c3d8a027367e1a6fc55d15336f04ff65c60c4f737b5739f7db4525c65fe9e"},
+ {file = "msgpack-1.0.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b88dc97ba86c96b964c3745a445d9a65f76fe21955a953064fe04adb63e9367"},
+ {file = "msgpack-1.0.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:00ce5f827d4f26fc094043e6f08b6069c1b148efa2631c47615ae14fb6cafc89"},
+ {file = "msgpack-1.0.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd6af61388be65a8701f5787362cb54adae20007e0cc67ca9221a4b95115583b"},
+ {file = "msgpack-1.0.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:652e4b7497825b0af6259e2c54700e6dc33d2fc4ed92b8839435090d4c9cc911"},
+ {file = "msgpack-1.0.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b08676a17e3f791daad34d5fcb18479e9c85e7200d5a17cbe8de798643a7e37"},
+ {file = "msgpack-1.0.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:229ccb6713c8b941eaa5cf13dc7478eba117f21513b5893c35e44483e2f0c9c8"},
+ {file = "msgpack-1.0.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:95ade0bd4cf69e04e8b8f8ec2d197d9c9c4a9b6902e048dc7456bf6d82e12a80"},
+ {file = "msgpack-1.0.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b16344032a27b2ccfd341f89dadf3e4ef6407d91e4b93563c14644a8abb3ad7"},
+ {file = "msgpack-1.0.6-cp311-cp311-win32.whl", hash = "sha256:55bb4a1bf94e39447bc08238a2fb8a767460388a8192f67c103442eb36920887"},
+ {file = "msgpack-1.0.6-cp311-cp311-win_amd64.whl", hash = "sha256:ae97504958d0bc58c1152045c170815d5c4f8af906561ce044b6358b43d0c97e"},
+ {file = "msgpack-1.0.6-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7ecf431786019a7bfedc28281531d706627f603e3691d64eccdbce3ecd353823"},
+ {file = "msgpack-1.0.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a635aecf1047255576dbb0927cbf9a7aa4a68e9d54110cc3c926652d18f144e0"},
+ {file = "msgpack-1.0.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:102cfb54eaefa73e8ca1e784b9352c623524185c98e057e519545131a56fb0af"},
+ {file = "msgpack-1.0.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c5e05e4f5756758c58a8088aa10dc70d851c89f842b611fdccfc0581c1846bc"},
+ {file = "msgpack-1.0.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68569509dd015fcdd1e6b2b3ccc8c51fd27d9a97f461ccc909270e220ee09685"},
+ {file = "msgpack-1.0.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf652839d16de91fe1cfb253e0a88db9a548796939533894e07f45d4bdf90a5f"},
+ {file = "msgpack-1.0.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14db7e1b7a7ed362b2f94897bf2486c899c8bb50f6e34b2db92fe534cdab306f"},
+ {file = "msgpack-1.0.6-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:159cfec18a6e125dd4723e2b1de6f202b34b87c850fb9d509acfd054c01135e9"},
+ {file = "msgpack-1.0.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6a01a072b2219b65a6ff74df208f20b2cac9401c60adb676ee34e53b4c651077"},
+ {file = "msgpack-1.0.6-cp312-cp312-win32.whl", hash = "sha256:e36560d001d4ba469d469b02037f2dd404421fd72277d9474efe9f03f83fced5"},
+ {file = "msgpack-1.0.6-cp312-cp312-win_amd64.whl", hash = "sha256:5e7fae9ca93258a956551708cf60dc6c8145574e32ce8c8c4d894e63bcb04341"},
+ {file = "msgpack-1.0.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:40b801b768f5a765e33c68f30665d3c6ee1c8623a2d2bb78e6e59f2db4e4ceb7"},
+ {file = "msgpack-1.0.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:da057d3652e698b00746e47f06dbb513314f847421e857e32e1dc61c46f6c052"},
+ {file = "msgpack-1.0.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f75114c05ec56566da6b55122791cf5bb53d5aada96a98c016d6231e03132f76"},
+ {file = "msgpack-1.0.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61213482b5a387ead9e250e9e3cb290292feca39dc83b41c3b1b7b8ffc8d8ecb"},
+ {file = "msgpack-1.0.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae6c561f11b444b258b1b4be2bdd1e1cf93cd1d80766b7e869a79db4543a8a8"},
+ {file = "msgpack-1.0.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:619a63753ba9e792fe3c6c0fc2b9ee2cfbd92153dd91bee029a89a71eb2942cd"},
+ {file = "msgpack-1.0.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:70843788c85ca385846a2d2f836efebe7bb2687ca0734648bf5c9dc6c55602d2"},
+ {file = "msgpack-1.0.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fb4571efe86545b772a4630fee578c213c91cbcfd20347806e47fd4e782a18fe"},
+ {file = "msgpack-1.0.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bbb4448a05d261fae423d5c0b0974ad899f60825bc77eabad5a0c518e78448c2"},
+ {file = "msgpack-1.0.6-cp38-cp38-win32.whl", hash = "sha256:5cd67674db3c73026e0a2c729b909780e88bd9cbc8184256f9567640a5d299a8"},
+ {file = "msgpack-1.0.6-cp38-cp38-win_amd64.whl", hash = "sha256:a1cf98afa7ad5e7012454ca3fde254499a13f9d92fd50cb46118118a249a1355"},
+ {file = "msgpack-1.0.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d6d25b8a5c70e2334ed61a8da4c11cd9b97c6fbd980c406033f06e4463fda006"},
+ {file = "msgpack-1.0.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:88cdb1da7fdb121dbb3116910722f5acab4d6e8bfcacab8fafe27e2e7744dc6a"},
+ {file = "msgpack-1.0.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b5658b1f9e486a2eec4c0c688f213a90085b9cf2fec76ef08f98fdf6c62f4b9"},
+ {file = "msgpack-1.0.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76820f2ece3b0a7c948bbb6a599020e29574626d23a649476def023cbb026787"},
+ {file = "msgpack-1.0.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c780d992f5d734432726b92a0c87bf1857c3d85082a8dea29cbf56e44a132b3"},
+ {file = "msgpack-1.0.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0ed35d6d6122d0baa9a1b59ebca4ee302139f4cfb57dab85e4c73ab793ae7ed"},
+ {file = "msgpack-1.0.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:32c0aff31f33033f4961abc01f78497e5e07bac02a508632aef394b384d27428"},
+ {file = "msgpack-1.0.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:35ad5aed9b52217d4cea739d0ea3a492a18dd86fecb4b132668a69f27fb0363b"},
+ {file = "msgpack-1.0.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47275ff73005a3e5e146e50baa2378e1730cba6e292f0222bc496a8e4c4adfc8"},
+ {file = "msgpack-1.0.6-cp39-cp39-win32.whl", hash = "sha256:7baf16fd8908a025c4a8d7b699103e72d41f967e2aee5a2065432bcdbd9fd06e"},
+ {file = "msgpack-1.0.6-cp39-cp39-win_amd64.whl", hash = "sha256:fc97aa4b4fb928ff4d3b74da7c30b360d0cb3ede49a5a6e1fd9705f49aea1deb"},
+ {file = "msgpack-1.0.6.tar.gz", hash = "sha256:25d3746da40f3c8c59c3b1d001e49fd2aa17904438f980d9a391370366df001e"},
+]
+
[[package]]
name = "multidict"
version = "6.0.4"
@@ -4187,67 +4838,69 @@ files = [
[[package]]
name = "pandas"
-version = "2.1.1"
+version = "2.0.3"
description = "Powerful data structures for data analysis, time series, and statistics"
optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.8"
files = [
- {file = "pandas-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58d997dbee0d4b64f3cb881a24f918b5f25dd64ddf31f467bb9b67ae4c63a1e4"},
- {file = "pandas-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02304e11582c5d090e5a52aec726f31fe3f42895d6bfc1f28738f9b64b6f0614"},
- {file = "pandas-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffa8f0966de2c22de408d0e322db2faed6f6e74265aa0856f3824813cf124363"},
- {file = "pandas-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1f84c144dee086fe4f04a472b5cd51e680f061adf75c1ae4fc3a9275560f8f4"},
- {file = "pandas-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ce97667d06d69396d72be074f0556698c7f662029322027c226fd7a26965cb"},
- {file = "pandas-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:4c3f32fd7c4dccd035f71734df39231ac1a6ff95e8bdab8d891167197b7018d2"},
- {file = "pandas-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e2959720b70e106bb1d8b6eadd8ecd7c8e99ccdbe03ee03260877184bb2877d"},
- {file = "pandas-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25e8474a8eb258e391e30c288eecec565bfed3e026f312b0cbd709a63906b6f8"},
- {file = "pandas-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8bd1685556f3374520466998929bade3076aeae77c3e67ada5ed2b90b4de7f0"},
- {file = "pandas-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc3657869c7902810f32bd072f0740487f9e030c1a3ab03e0af093db35a9d14e"},
- {file = "pandas-2.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:05674536bd477af36aa2effd4ec8f71b92234ce0cc174de34fd21e2ee99adbc2"},
- {file = "pandas-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:b407381258a667df49d58a1b637be33e514b07f9285feb27769cedb3ab3d0b3a"},
- {file = "pandas-2.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c747793c4e9dcece7bb20156179529898abf505fe32cb40c4052107a3c620b49"},
- {file = "pandas-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3bcad1e6fb34b727b016775bea407311f7721db87e5b409e6542f4546a4951ea"},
- {file = "pandas-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5ec7740f9ccb90aec64edd71434711f58ee0ea7f5ed4ac48be11cfa9abf7317"},
- {file = "pandas-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29deb61de5a8a93bdd033df328441a79fcf8dd3c12d5ed0b41a395eef9cd76f0"},
- {file = "pandas-2.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f99bebf19b7e03cf80a4e770a3e65eee9dd4e2679039f542d7c1ace7b7b1daa"},
- {file = "pandas-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:84e7e910096416adec68075dc87b986ff202920fb8704e6d9c8c9897fe7332d6"},
- {file = "pandas-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366da7b0e540d1b908886d4feb3d951f2f1e572e655c1160f5fde28ad4abb750"},
- {file = "pandas-2.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e50e72b667415a816ac27dfcfe686dc5a0b02202e06196b943d54c4f9c7693e"},
- {file = "pandas-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc1ab6a25da197f03ebe6d8fa17273126120874386b4ac11c1d687df288542dd"},
- {file = "pandas-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0dbfea0dd3901ad4ce2306575c54348d98499c95be01b8d885a2737fe4d7a98"},
- {file = "pandas-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0489b0e6aa3d907e909aef92975edae89b1ee1654db5eafb9be633b0124abe97"},
- {file = "pandas-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:4cdb0fab0400c2cb46dafcf1a0fe084c8bb2480a1fa8d81e19d15e12e6d4ded2"},
- {file = "pandas-2.1.1.tar.gz", hash = "sha256:fecb198dc389429be557cde50a2d46da8434a17fe37d7d41ff102e3987fd947b"},
+ {file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"},
+ {file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"},
+ {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"},
+ {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"},
+ {file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"},
+ {file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"},
+ {file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"},
+ {file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"},
+ {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"},
+ {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"},
+ {file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"},
+ {file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"},
+ {file = "pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061"},
+ {file = "pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5"},
+ {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089"},
+ {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0"},
+ {file = "pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02"},
+ {file = "pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78"},
+ {file = "pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b"},
+ {file = "pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e"},
+ {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b"},
+ {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641"},
+ {file = "pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682"},
+ {file = "pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc"},
+ {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"},
]
[package.dependencies]
-numpy = {version = ">=1.22.4", markers = "python_version < \"3.11\""}
+numpy = [
+ {version = ">=1.20.3", markers = "python_version < \"3.10\""},
+ {version = ">=1.21.0", markers = "python_version >= \"3.10\""},
+]
python-dateutil = ">=2.8.2"
pytz = ">=2020.1"
tzdata = ">=2022.1"
[package.extras]
-all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"]
-aws = ["s3fs (>=2022.05.0)"]
-clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"]
-compression = ["zstandard (>=0.17.0)"]
-computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"]
-consortium-standard = ["dataframe-api-compat (>=0.1.7)"]
-excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"]
+all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"]
+aws = ["s3fs (>=2021.08.0)"]
+clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"]
+compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"]
+computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"]
+excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"]
feather = ["pyarrow (>=7.0.0)"]
-fss = ["fsspec (>=2022.05.0)"]
-gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"]
-hdf5 = ["tables (>=3.7.0)"]
-html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"]
-mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"]
-output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"]
+fss = ["fsspec (>=2021.07.0)"]
+gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"]
+hdf5 = ["tables (>=3.6.1)"]
+html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"]
+mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"]
+output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"]
parquet = ["pyarrow (>=7.0.0)"]
-performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"]
+performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"]
plot = ["matplotlib (>=3.6.1)"]
-postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"]
-spss = ["pyreadstat (>=1.1.5)"]
-sql-other = ["SQLAlchemy (>=1.4.36)"]
-test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"]
-xml = ["lxml (>=4.8.0)"]
+postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"]
+spss = ["pyreadstat (>=1.1.2)"]
+sql-other = ["SQLAlchemy (>=1.4.16)"]
+test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"]
+xml = ["lxml (>=4.6.3)"]
[[package]]
name = "pandas-stubs"
@@ -5657,6 +6310,24 @@ python-dateutil = ">=2.8.1,<3.0.0"
typing-extensions = ">=4.2.0,<5.0.0"
websockets = ">=10.3,<11.0"
+[[package]]
+name = "redis"
+version = "4.6.0"
+description = "Python client for Redis database and key-value store"
+optional = true
+python-versions = ">=3.7"
+files = [
+ {file = "redis-4.6.0-py3-none-any.whl", hash = "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c"},
+ {file = "redis-4.6.0.tar.gz", hash = "sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d"},
+]
+
+[package.dependencies]
+async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""}
+
+[package.extras]
+hiredis = ["hiredis (>=1.0.0)"]
+ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
+
[[package]]
name = "regex"
version = "2023.8.8"
@@ -5824,6 +6495,16 @@ pygments = ">=2.13.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
+[[package]]
+name = "roundrobin"
+version = "0.0.4"
+description = "Collection of roundrobin utilities"
+optional = false
+python-versions = "*"
+files = [
+ {file = "roundrobin-0.0.4.tar.gz", hash = "sha256:7e9d19a5bd6123d99993fb935fa86d25c88bb2096e493885f61737ed0f5e9abd"},
+]
+
[[package]]
name = "rsa"
version = "4.9"
@@ -7135,6 +7816,20 @@ files = [
{file = "types_pyasn1-0.4.0.6-py3-none-any.whl", hash = "sha256:dd5fc818864e63a66cd714be0a7a59a493f4a81b87ee9ac978c41f1eaa9a0cef"},
]
+[[package]]
+name = "types-pyopenssl"
+version = "23.2.0.2"
+description = "Typing stubs for pyOpenSSL"
+optional = false
+python-versions = "*"
+files = [
+ {file = "types-pyOpenSSL-23.2.0.2.tar.gz", hash = "sha256:6a010dac9ecd42b582d7dd2cc3e9e40486b79b3b64bb2fffba1474ff96af906d"},
+ {file = "types_pyOpenSSL-23.2.0.2-py3-none-any.whl", hash = "sha256:19536aa3debfbe25a918cf0d898e9f5fbbe6f3594a429da7914bf331deb1b342"},
+]
+
+[package.dependencies]
+cryptography = ">=35.0.0"
+
[[package]]
name = "types-python-jose"
version = "3.3.4.8"
@@ -7182,6 +7877,21 @@ files = [
{file = "types_PyYAML-6.0.12.11-py3-none-any.whl", hash = "sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d"},
]
+[[package]]
+name = "types-redis"
+version = "4.6.0.6"
+description = "Typing stubs for redis"
+optional = false
+python-versions = "*"
+files = [
+ {file = "types-redis-4.6.0.6.tar.gz", hash = "sha256:7865a843802937ab2ddca33579c4e255bfe73f87af85824ead7a6729ba92fc52"},
+ {file = "types_redis-4.6.0.6-py3-none-any.whl", hash = "sha256:e0e9dcc530623db3a41ec058ccefdcd5c7582557f02ab5f7aa9a27fe10a78d7e"},
+]
+
+[package.dependencies]
+cryptography = ">=35.0.0"
+types-pyOpenSSL = "*"
+
[[package]]
name = "types-requests"
version = "2.31.0.3"
@@ -7445,6 +8155,17 @@ testing = ["pytest (>=7.4.0)"]
tooling = ["black (>=23.7.0)", "pyright (>=1.1.325)", "ruff (>=0.0.287)"]
tooling-extras = ["pyaml (>=23.7.0)", "pypandoc-binary (>=1.11)", "pytest (>=7.4.0)"]
+[[package]]
+name = "vine"
+version = "5.0.0"
+description = "Promises, promises, promises."
+optional = true
+python-versions = ">=3.6"
+files = [
+ {file = "vine-5.0.0-py2.py3-none-any.whl", hash = "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30"},
+ {file = "vine-5.0.0.tar.gz", hash = "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"},
+]
+
[[package]]
name = "watchfiles"
version = "0.20.0"
@@ -7603,6 +8324,23 @@ files = [
{file = "websockets-10.4.tar.gz", hash = "sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3"},
]
+[[package]]
+name = "werkzeug"
+version = "2.3.7"
+description = "The comprehensive WSGI web application library."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "werkzeug-2.3.7-py3-none-any.whl", hash = "sha256:effc12dba7f3bd72e605ce49807bbe692bd729c3bb122a3b91747a6ae77df528"},
+ {file = "werkzeug-2.3.7.tar.gz", hash = "sha256:2b8c0e447b4b9dbcc85dd97b6eeb4dcbaf6c8b6c3be0bd654e25553e0a2157d8"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.1.1"
+
+[package.extras]
+watchdog = ["watchdog (>=2.3)"]
+
[[package]]
name = "wheel"
version = "0.41.2"
@@ -7831,6 +8569,71 @@ files = [
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
+[[package]]
+name = "zope-event"
+version = "5.0"
+description = "Very basic event publishing system"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26"},
+ {file = "zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"},
+]
+
+[package.dependencies]
+setuptools = "*"
+
+[package.extras]
+docs = ["Sphinx"]
+test = ["zope.testrunner"]
+
+[[package]]
+name = "zope-interface"
+version = "6.0"
+description = "Interfaces for Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "zope.interface-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f299c020c6679cb389814a3b81200fe55d428012c5e76da7e722491f5d205990"},
+ {file = "zope.interface-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee4b43f35f5dc15e1fec55ccb53c130adb1d11e8ad8263d68b1284b66a04190d"},
+ {file = "zope.interface-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a158846d0fca0a908c1afb281ddba88744d403f2550dc34405c3691769cdd85"},
+ {file = "zope.interface-6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f72f23bab1848edb7472309e9898603141644faec9fd57a823ea6b4d1c4c8995"},
+ {file = "zope.interface-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48f4d38cf4b462e75fac78b6f11ad47b06b1c568eb59896db5b6ec1094eb467f"},
+ {file = "zope.interface-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:87b690bbee9876163210fd3f500ee59f5803e4a6607d1b1238833b8885ebd410"},
+ {file = "zope.interface-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2363e5fd81afb650085c6686f2ee3706975c54f331b426800b53531191fdf28"},
+ {file = "zope.interface-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af169ba897692e9cd984a81cb0f02e46dacdc07d6cf9fd5c91e81f8efaf93d52"},
+ {file = "zope.interface-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa90bac61c9dc3e1a563e5babb3fd2c0c1c80567e815442ddbe561eadc803b30"},
+ {file = "zope.interface-6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89086c9d3490a0f265a3c4b794037a84541ff5ffa28bb9c24cc9f66566968464"},
+ {file = "zope.interface-6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:809fe3bf1a91393abc7e92d607976bbb8586512913a79f2bf7d7ec15bd8ea518"},
+ {file = "zope.interface-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:0ec9653825f837fbddc4e4b603d90269b501486c11800d7c761eee7ce46d1bbb"},
+ {file = "zope.interface-6.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:790c1d9d8f9c92819c31ea660cd43c3d5451df1df61e2e814a6f99cebb292788"},
+ {file = "zope.interface-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b39b8711578dcfd45fc0140993403b8a81e879ec25d53189f3faa1f006087dca"},
+ {file = "zope.interface-6.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eba51599370c87088d8882ab74f637de0c4f04a6d08a312dce49368ba9ed5c2a"},
+ {file = "zope.interface-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee934f023f875ec2cfd2b05a937bd817efcc6c4c3f55c5778cbf78e58362ddc"},
+ {file = "zope.interface-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:042f2381118b093714081fd82c98e3b189b68db38ee7d35b63c327c470ef8373"},
+ {file = "zope.interface-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dfbbbf0809a3606046a41f8561c3eada9db811be94138f42d9135a5c47e75f6f"},
+ {file = "zope.interface-6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:424d23b97fa1542d7be882eae0c0fc3d6827784105264a8169a26ce16db260d8"},
+ {file = "zope.interface-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e538f2d4a6ffb6edfb303ce70ae7e88629ac6e5581870e66c306d9ad7b564a58"},
+ {file = "zope.interface-6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12175ca6b4db7621aedd7c30aa7cfa0a2d65ea3a0105393e05482d7a2d367446"},
+ {file = "zope.interface-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3d7dfd897a588ec27e391edbe3dd320a03684457470415870254e714126b1f"},
+ {file = "zope.interface-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b3f543ae9d3408549a9900720f18c0194ac0fe810cecda2a584fd4dca2eb3bb8"},
+ {file = "zope.interface-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0583b75f2e70ec93f100931660328965bb9ff65ae54695fb3fa0a1255daa6f2"},
+ {file = "zope.interface-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:23ac41d52fd15dd8be77e3257bc51bbb82469cf7f5e9a30b75e903e21439d16c"},
+ {file = "zope.interface-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99856d6c98a326abbcc2363827e16bd6044f70f2ef42f453c0bd5440c4ce24e5"},
+ {file = "zope.interface-6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1592f68ae11e557b9ff2bc96ac8fc30b187e77c45a3c9cd876e3368c53dc5ba8"},
+ {file = "zope.interface-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4407b1435572e3e1610797c9203ad2753666c62883b921318c5403fb7139dec2"},
+ {file = "zope.interface-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:5171eb073474a5038321409a630904fd61f12dd1856dd7e9d19cd6fe092cbbc5"},
+ {file = "zope.interface-6.0.tar.gz", hash = "sha256:aab584725afd10c710b8f1e6e208dbee2d0ad009f57d674cb9d1b3964037275d"},
+]
+
+[package.dependencies]
+setuptools = "*"
+
+[package.extras]
+docs = ["Sphinx", "repoze.sphinx.autointerface"]
+test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
+testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
+
[[package]]
name = "zstandard"
version = "0.21.0"
@@ -7891,10 +8694,10 @@ cffi = ["cffi (>=1.11)"]
[extras]
all = []
-deploy = ["langchain-serve"]
+deploy = ["celery", "flower", "langchain-serve", "redis"]
local = ["ctransformers", "llama-cpp-python", "sentence-transformers"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<3.11"
-content-hash = "7aac1af43e599106fa9e6ee38978d014608b0275b277201d605484e675c00cea"
+content-hash = "ebf83fe66c2a78ceffc2c9228b439db10b37d0e444dd3f275e1a1addab1fd164"
diff --git a/pyproject.toml b/pyproject.toml
index feacbb12e..b39111715 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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]
diff --git a/scripts/deploy_langflow_gcp.sh b/scripts/gcp/deploy_langflow_gcp.sh
similarity index 100%
rename from scripts/deploy_langflow_gcp.sh
rename to scripts/gcp/deploy_langflow_gcp.sh
diff --git a/scripts/deploy_langflow_gcp_spot.sh b/scripts/gcp/deploy_langflow_gcp_spot.sh
similarity index 100%
rename from scripts/deploy_langflow_gcp_spot.sh
rename to scripts/gcp/deploy_langflow_gcp_spot.sh
diff --git a/scripts/walkthroughtutorial.md b/scripts/gcp/walkthroughtutorial.md
similarity index 100%
rename from scripts/walkthroughtutorial.md
rename to scripts/gcp/walkthroughtutorial.md
diff --git a/scripts/walkthroughtutorial_spot.md b/scripts/gcp/walkthroughtutorial_spot.md
similarity index 100%
rename from scripts/walkthroughtutorial_spot.md
rename to scripts/gcp/walkthroughtutorial_spot.md
diff --git a/src/backend/langflow/__init__.py b/src/backend/langflow/__init__.py
index f6eb836cc..d3afbb4af 100644
--- a/src/backend/langflow/__init__.py
+++ b/src/backend/langflow/__init__.py
@@ -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"]
diff --git a/src/backend/langflow/__main__.py b/src/backend/langflow/__main__.py
index 84adf7bee..c17d39d91 100644
--- a/src/backend/langflow/__main__.py
+++ b/src/backend/langflow/__main__.py
@@ -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)
diff --git a/src/backend/langflow/api/utils.py b/src/backend/langflow/api/utils.py
index 0fb53e541..c519fffed 100644
--- a/src/backend/langflow/api/utils.py
+++ b/src/backend/langflow/api/utils.py
@@ -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) + ")"
diff --git a/src/backend/langflow/api/v1/chat.py b/src/backend/langflow/api/v1/chat.py
index adc6b3d61..a7a50a39f 100644
--- a/src/backend/langflow/api/v1/chat.py
+++ b/src/backend/langflow/api/v1/chat.py
@@ -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
diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py
index 0d1ad751f..ff5e0541e 100644
--- a/src/backend/langflow/api/v1/endpoints.py
+++ b/src/backend/langflow/api/v1/endpoints.py
@@ -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()
diff --git a/src/backend/langflow/api/v1/flows.py b/src/backend/langflow/api/v1/flows.py
index c323dae53..9953db0db 100644
--- a/src/backend/langflow/api/v1/flows.py
+++ b/src/backend/langflow/api/v1/flows.py
@@ -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:
diff --git a/src/backend/langflow/api/v1/login.py b/src/backend/langflow/api/v1/login.py
index 9ff059bf9..4dfc723b5 100644
--- a/src/backend/langflow/api/v1/login.py
+++ b/src/backend/langflow/api/v1/login.py
@@ -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(
diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py
index c0867feb5..519bc534e 100644
--- a/src/backend/langflow/api/v1/schemas.py
+++ b/src/backend/langflow/api/v1/schemas.py
@@ -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):
diff --git a/src/backend/langflow/api/v1/users.py b/src/backend/langflow/api/v1/users.py
index e1e24d197..73c7346d9 100644
--- a/src/backend/langflow/api/v1/users.py
+++ b/src/backend/langflow/api/v1/users.py
@@ -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
diff --git a/src/backend/langflow/core/__init__.py b/src/backend/langflow/core/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/backend/langflow/core/celery_app.py b/src/backend/langflow/core/celery_app.py
new file mode 100644
index 000000000..ef3fc6545
--- /dev/null
+++ b/src/backend/langflow/core/celery_app.py
@@ -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")
diff --git a/src/backend/langflow/core/celeryconfig.py b/src/backend/langflow/core/celeryconfig.py
new file mode 100644
index 000000000..35d51bba0
--- /dev/null
+++ b/src/backend/langflow/core/celeryconfig.py
@@ -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"]
diff --git a/src/backend/langflow/graph/edge/base.py b/src/backend/langflow/graph/edge/base.py
index 2df20cbde..2c60b0288 100644
--- a/src/backend/langflow/graph/edge/base.py
+++ b/src/backend/langflow/graph/edge/base.py
@@ -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
diff --git a/src/backend/langflow/graph/graph/base.py b/src/backend/langflow/graph/graph/base.py
index 94964e472..227b04bf9 100644
--- a/src/backend/langflow/graph/graph/base.py
+++ b/src/backend/langflow/graph/graph/base.py
@@ -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]:
diff --git a/src/backend/langflow/graph/vertex/base.py b/src/backend/langflow/graph/vertex/base.py
index ac05ac221..3877e59b1 100644
--- a/src/backend/langflow/graph/vertex/base.py
+++ b/src/backend/langflow/graph/vertex/base.py
@@ -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)
diff --git a/src/backend/langflow/graph/vertex/types.py b/src/backend/langflow/graph/vertex/types.py
index d5b7d2d58..7609982a5 100644
--- a/src/backend/langflow/graph/vertex/types.py
+++ b/src/backend/langflow/graph/vertex/types.py
@@ -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()
diff --git a/src/backend/langflow/graph/vertex/utils.py b/src/backend/langflow/graph/vertex/utils.py
new file mode 100644
index 000000000..e1d439f4b
--- /dev/null
+++ b/src/backend/langflow/graph/vertex/utils.py
@@ -0,0 +1,5 @@
+from langflow.utils.constants import PYTHON_BASIC_TYPES
+
+
+def is_basic_type(obj):
+ return type(obj) in PYTHON_BASIC_TYPES
diff --git a/src/backend/langflow/interface/agents/base.py b/src/backend/langflow/interface/agents/base.py
index f48015b2c..696f5afd0 100644
--- a/src/backend/langflow/interface/agents/base.py
+++ b/src/backend/langflow/interface/agents/base.py
@@ -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
diff --git a/src/backend/langflow/interface/base.py b/src/backend/langflow/interface/base.py
index 4bb657b3c..13dd05619 100644
--- a/src/backend/langflow/interface/base.py
+++ b/src/backend/langflow/interface/base.py
@@ -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"]
diff --git a/src/backend/langflow/interface/chains/base.py b/src/backend/langflow/interface/chains/base.py
index 99b0a693f..b2d07b7e4 100644
--- a/src/backend/langflow/interface/chains/base.py
+++ b/src/backend/langflow/interface/chains/base.py
@@ -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
diff --git a/src/backend/langflow/interface/custom/custom_component.py b/src/backend/langflow/interface/custom/custom_component.py
index 9de09507a..49ac81ab1 100644
--- a/src/backend/langflow/interface/custom/custom_component.py
+++ b/src/backend/langflow/interface/custom/custom_component.py
@@ -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:
diff --git a/src/backend/langflow/interface/document_loaders/base.py b/src/backend/langflow/interface/document_loaders/base.py
index 05311444b..e2c379b67 100644
--- a/src/backend/langflow/interface/document_loaders/base.py
+++ b/src/backend/langflow/interface/document_loaders/base.py
@@ -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
]
diff --git a/src/backend/langflow/interface/embeddings/base.py b/src/backend/langflow/interface/embeddings/base.py
index 0145d9859..d280cf1c1 100644
--- a/src/backend/langflow/interface/embeddings/base.py
+++ b/src/backend/langflow/interface/embeddings/base.py
@@ -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
]
diff --git a/src/backend/langflow/interface/initialize/loading.py b/src/backend/langflow/interface/initialize/loading.py
index b600fb5b2..82075cd2e 100644
--- a/src/backend/langflow/interface/initialize/loading.py
+++ b/src/backend/langflow/interface/initialize/loading.py
@@ -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:
diff --git a/src/backend/langflow/interface/initialize/vector_store.py b/src/backend/langflow/interface/initialize/vector_store.py
index 233292626..59e7c0d3b 100644
--- a/src/backend/langflow/interface/initialize/vector_store.py
+++ b/src/backend/langflow/interface/initialize/vector_store.py
@@ -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()
diff --git a/src/backend/langflow/interface/listing.py b/src/backend/langflow/interface/listing.py
index 1cab1efbc..aa72e568e 100644
--- a/src/backend/langflow/interface/listing.py
+++ b/src/backend/langflow/interface/listing.py
@@ -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()
diff --git a/src/backend/langflow/interface/llms/base.py b/src/backend/langflow/interface/llms/base.py
index 17a2ae0ee..ffb7fa2f2 100644
--- a/src/backend/langflow/interface/llms/base.py
+++ b/src/backend/langflow/interface/llms/base.py
@@ -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
]
diff --git a/src/backend/langflow/interface/memories/base.py b/src/backend/langflow/interface/memories/base.py
index 6c826d0ac..3f3658304 100644
--- a/src/backend/langflow/interface/memories/base.py
+++ b/src/backend/langflow/interface/memories/base.py
@@ -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
]
diff --git a/src/backend/langflow/interface/output_parsers/base.py b/src/backend/langflow/interface/output_parsers/base.py
index 48bcd1896..06dfdf4eb 100644
--- a/src/backend/langflow/interface/output_parsers/base.py
+++ b/src/backend/langflow/interface/output_parsers/base.py
@@ -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
diff --git a/src/backend/langflow/interface/prompts/base.py b/src/backend/langflow/interface/prompts/base.py
index d74e0c1e8..29d3e8ba8 100644
--- a/src/backend/langflow/interface/prompts/base.py
+++ b/src/backend/langflow/interface/prompts/base.py
@@ -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
diff --git a/src/backend/langflow/interface/retrievers/base.py b/src/backend/langflow/interface/retrievers/base.py
index 415a7fda8..4ee40e659 100644
--- a/src/backend/langflow/interface/retrievers/base.py
+++ b/src/backend/langflow/interface/retrievers/base.py
@@ -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
]
diff --git a/src/backend/langflow/interface/run.py b/src/backend/langflow/interface/run.py
index eb012e182..63391204a 100644
--- a/src/backend/langflow/interface/run.py
+++ b/src/backend/langflow/interface/run.py
@@ -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):
diff --git a/src/backend/langflow/interface/text_splitters/base.py b/src/backend/langflow/interface/text_splitters/base.py
index fba4e32cc..4f337817f 100644
--- a/src/backend/langflow/interface/text_splitters/base.py
+++ b/src/backend/langflow/interface/text_splitters/base.py
@@ -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
]
diff --git a/src/backend/langflow/interface/toolkits/base.py b/src/backend/langflow/interface/toolkits/base.py
index be602cb7c..73ca4852f 100644
--- a/src/backend/langflow/interface/toolkits/base.py
+++ b/src/backend/langflow/interface/toolkits/base.py
@@ -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
diff --git a/src/backend/langflow/interface/tools/base.py b/src/backend/langflow/interface/tools/base.py
index 999f93703..a99025ff7 100644
--- a/src/backend/langflow/interface/tools/base.py
+++ b/src/backend/langflow/interface/tools/base.py
@@ -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
diff --git a/src/backend/langflow/interface/tools/util.py b/src/backend/langflow/interface/tools/util.py
index fc91387ed..8e4f582c1 100644
--- a/src/backend/langflow/interface/tools/util.py
+++ b/src/backend/langflow/interface/tools/util.py
@@ -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]:
diff --git a/src/backend/langflow/interface/types.py b/src/backend/langflow/interface/types.py
index fa9967649..25d3e8c8b 100644
--- a/src/backend/langflow/interface/types.py
+++ b/src/backend/langflow/interface/types.py
@@ -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
diff --git a/src/backend/langflow/interface/utilities/base.py b/src/backend/langflow/interface/utilities/base.py
index 3cec49be9..665143da2 100644
--- a/src/backend/langflow/interface/utilities/base.py
+++ b/src/backend/langflow/interface/utilities/base.py
@@ -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
diff --git a/src/backend/langflow/interface/utils.py b/src/backend/langflow/interface/utils.py
index f993c971b..7b78e75c5 100644
--- a/src/backend/langflow/interface/utils.py
+++ b/src/backend/langflow/interface/utils.py
@@ -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__}")
diff --git a/src/backend/langflow/interface/vector_store/base.py b/src/backend/langflow/interface/vector_store/base.py
index 06b8668f3..786031a6b 100644
--- a/src/backend/langflow/interface/vector_store/base.py
+++ b/src/backend/langflow/interface/vector_store/base.py
@@ -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
]
diff --git a/src/backend/langflow/processing/process.py b/src/backend/langflow/processing/process.py
index 911496d23..4c96e2fde 100644
--- a/src/backend/langflow/processing/process.py
+++ b/src/backend/langflow/processing/process.py
@@ -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(
diff --git a/src/backend/langflow/services/auth/factory.py b/src/backend/langflow/services/auth/factory.py
index 4914ce645..b44019289 100644
--- a/src/backend/langflow/services/auth/factory.py
+++ b/src/backend/langflow/services/auth/factory.py
@@ -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)
diff --git a/src/backend/langflow/services/auth/service.py b/src/backend/langflow/services/auth/service.py
index 29984a75c..5b0acf8c6 100644
--- a/src/backend/langflow/services/auth/service.py
+++ b/src/backend/langflow/services/auth/service.py
@@ -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
diff --git a/src/backend/langflow/services/auth/utils.py b/src/backend/langflow/services/auth/utils.py
index 801874c7d..f88a1cd12 100644
--- a/src/backend/langflow/services/auth/utils.py
+++ b/src/backend/langflow/services/auth/utils.py
@@ -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
diff --git a/src/backend/langflow/services/cache/__init__.py b/src/backend/langflow/services/cache/__init__.py
index 79e143807..3b122aa9e 100644
--- a/src/backend/langflow/services/cache/__init__.py
+++ b/src/backend/langflow/services/cache/__init__.py
@@ -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",
diff --git a/src/backend/langflow/services/cache/base.py b/src/backend/langflow/services/cache/base.py
index 88cb3a1da..4eee6639e 100644
--- a/src/backend/langflow/services/cache/base.py
+++ b/src/backend/langflow/services/cache/base.py
@@ -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):
"""
diff --git a/src/backend/langflow/services/cache/factory.py b/src/backend/langflow/services/cache/factory.py
index f180f67c0..f00ab239f 100644
--- a/src/backend/langflow/services/cache/factory.py
+++ b/src/backend/langflow/services/cache/factory.py
@@ -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()
diff --git a/src/backend/langflow/services/cache/flow.py b/src/backend/langflow/services/cache/flow.py
deleted file mode 100644
index 0c10c51e1..000000000
--- a/src/backend/langflow/services/cache/flow.py
+++ /dev/null
@@ -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})"
diff --git a/src/backend/langflow/services/cache/manager.py b/src/backend/langflow/services/cache/manager.py
index ce9a338ef..19f5034cf 100644
--- a/src/backend/langflow/services/cache/manager.py
+++ b/src/backend/langflow/services/cache/manager.py
@@ -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})"
diff --git a/src/backend/langflow/services/cache/utils.py b/src/backend/langflow/services/cache/utils.py
index 4342914cd..75380b3d5 100644
--- a/src/backend/langflow/services/cache/utils.py
+++ b/src/backend/langflow/services/cache/utils.py
@@ -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")
diff --git a/src/backend/langflow/services/chat/cache.py b/src/backend/langflow/services/chat/cache.py
new file mode 100644
index 000000000..f6247b540
--- /dev/null
+++ b/src/backend/langflow/services/chat/cache.py
@@ -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()
diff --git a/src/backend/langflow/services/chat/factory.py b/src/backend/langflow/services/chat/factory.py
index ca844893a..54af7fcca 100644
--- a/src/backend/langflow/services/chat/factory.py
+++ b/src/backend/langflow/services/chat/factory.py
@@ -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()
diff --git a/src/backend/langflow/services/chat/manager.py b/src/backend/langflow/services/chat/manager.py
index 19775bede..3fe937ce8 100644
--- a/src/backend/langflow/services/chat/manager.py
+++ b/src/backend/langflow/services/chat/manager.py
@@ -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}")
diff --git a/src/backend/langflow/services/database/factory.py b/src/backend/langflow/services/database/factory.py
index 25427b7b9..3726f520b 100644
--- a/src/backend/langflow/services/database/factory.py
+++ b/src/backend/langflow/services/database/factory.py
@@ -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)
diff --git a/src/backend/langflow/services/database/manager.py b/src/backend/langflow/services/database/manager.py
index 3c842bf52..ca7f34d10 100644
--- a/src/backend/langflow/services/database/manager.py
+++ b/src/backend/langflow/services/database/manager.py
@@ -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)
diff --git a/src/backend/langflow/services/database/models/api_key/api_key.py b/src/backend/langflow/services/database/models/api_key/api_key.py
index 5d5bab0f4..35aa6c7a9 100644
--- a/src/backend/langflow/services/database/models/api_key/api_key.py
+++ b/src/backend/langflow/services/database/models/api_key/api_key.py
@@ -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):
diff --git a/src/backend/langflow/services/database/models/api_key/crud.py b/src/backend/langflow/services/database/models/api_key/crud.py
index abc84f108..c1c6f786e 100644
--- a/src/backend/langflow/services/database/models/api_key/crud.py
+++ b/src/backend/langflow/services/database/models/api_key/crud.py
@@ -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
diff --git a/src/backend/langflow/services/database/models/user/user.py b/src/backend/langflow/services/database/models/user/user.py
index 8bd42af00..5f2f3e1c4 100644
--- a/src/backend/langflow/services/database/models/user/user.py
+++ b/src/backend/langflow/services/database/models/user/user.py
@@ -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()
diff --git a/src/backend/langflow/services/database/utils.py b/src/backend/langflow/services/database/utils.py
index fd0a8856a..ce4990fa1 100644
--- a/src/backend/langflow/services/database/utils.py
+++ b/src/backend/langflow/services/database/utils.py
@@ -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)
diff --git a/src/backend/langflow/services/getters.py b/src/backend/langflow/services/getters.py
index 8b32aef02..e88b998b5 100644
--- a/src/backend/langflow/services/getters.py
+++ b/src/backend/langflow/services/getters.py
@@ -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)
diff --git a/src/backend/langflow/services/manager.py b/src/backend/langflow/services/manager.py
index 9398a10f4..e74146a20 100644
--- a/src/backend/langflow/services/manager.py
+++ b/src/backend/langflow/services/manager.py
@@ -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()
diff --git a/src/backend/langflow/services/plugins/langfuse.py b/src/backend/langflow/services/plugins/langfuse.py
index 333459080..29d59808b 100644
--- a/src/backend/langflow/services/plugins/langfuse.py
+++ b/src/backend/langflow/services/plugins/langfuse.py
@@ -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
diff --git a/src/backend/langflow/services/schema.py b/src/backend/langflow/services/schema.py
index 6291a0d0b..8b3b41fcb 100644
--- a/src/backend/langflow/services/schema.py
+++ b/src/backend/langflow/services/schema.py
@@ -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"
diff --git a/src/backend/langflow/services/session/__init__.py b/src/backend/langflow/services/session/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/backend/langflow/services/session/factory.py b/src/backend/langflow/services/session/factory.py
new file mode 100644
index 000000000..9abe025a8
--- /dev/null
+++ b/src/backend/langflow/services/session/factory.py
@@ -0,0 +1,14 @@
+from typing import TYPE_CHECKING
+from langflow.services.session.manager import SessionService
+from langflow.services.factory import ServiceFactory
+
+if TYPE_CHECKING:
+ from langflow.services.cache.manager import BaseCacheService
+
+
+class SessionServiceFactory(ServiceFactory):
+ def __init__(self):
+ super().__init__(SessionService)
+
+ def create(self, cache_service: "BaseCacheService"):
+ return SessionService(cache_service)
diff --git a/src/backend/langflow/services/session/manager.py b/src/backend/langflow/services/session/manager.py
new file mode 100644
index 000000000..6bdebf6b3
--- /dev/null
+++ b/src/backend/langflow/services/session/manager.py
@@ -0,0 +1,47 @@
+from typing import TYPE_CHECKING
+from langflow.interface.run import build_sorted_vertices
+from langflow.services.base import Service
+from langflow.services.cache.utils import compute_dict_hash
+from langflow.services.session.utils import session_id_generator
+
+if TYPE_CHECKING:
+ from langflow.services.cache.base import BaseCacheService
+
+
+class SessionService(Service):
+ name = "session_service"
+
+ def __init__(self, cache_service):
+ self.cache_service: "BaseCacheService" = cache_service
+
+ def load_session(self, key, data_graph):
+ # Check if the data is cached
+ if key in self.cache_service:
+ return self.cache_service.get(key)
+
+ if key is None:
+ key = self.generate_key(session_id=None, data_graph=data_graph)
+
+ # If not cached, build the graph and cache it
+ graph, artifacts = build_sorted_vertices(data_graph)
+
+ self.cache_service.set(key, (graph, artifacts))
+
+ return graph, artifacts
+
+ def build_key(self, session_id, data_graph):
+ json_hash = compute_dict_hash(data_graph)
+ return f"{session_id}{':' if session_id else ''}{json_hash}"
+
+ def generate_key(self, session_id, data_graph):
+ # Hash the JSON and combine it with the session_id to create a unique key
+ if session_id is None:
+ # generate a 5 char session_id to concatenate with the json_hash
+ session_id = session_id_generator()
+ return self.build_key(session_id, data_graph=data_graph)
+
+ def update_session(self, session_id, value):
+ self.cache_service.set(session_id, value)
+
+ def clear_session(self, session_id):
+ self.cache_service.delete(session_id)
diff --git a/src/backend/langflow/services/session/utils.py b/src/backend/langflow/services/session/utils.py
new file mode 100644
index 000000000..374d85540
--- /dev/null
+++ b/src/backend/langflow/services/session/utils.py
@@ -0,0 +1,8 @@
+import random
+import string
+
+
+def session_id_generator(size=6):
+ return "".join(
+ random.SystemRandom().choices(string.ascii_uppercase + string.digits, k=size)
+ )
diff --git a/src/backend/langflow/services/settings/auth.py b/src/backend/langflow/services/settings/auth.py
index b6d288183..4a3eee100 100644
--- a/src/backend/langflow/services/settings/auth.py
+++ b/src/backend/langflow/services/settings/auth.py
@@ -35,6 +35,7 @@ class AuthSettings(BaseSettings):
# If AUTO_LOGIN = True
# > The application does not request login and logs in automatically as a super user.
AUTO_LOGIN: bool = False
+ NEW_USER_IS_ACTIVE: bool = False
SUPERUSER: str = DEFAULT_SUPERUSER
SUPERUSER_PASSWORD: str = DEFAULT_SUPERUSER_PASSWORD
diff --git a/src/backend/langflow/services/settings/base.py b/src/backend/langflow/services/settings/base.py
index 86cbf64e5..14e3f9928 100644
--- a/src/backend/langflow/services/settings/base.py
+++ b/src/backend/langflow/services/settings/base.py
@@ -37,9 +37,16 @@ class Settings(BaseSettings):
DEV: bool = False
DATABASE_URL: Optional[str] = None
- CACHE: Optional[str] = None
+ CACHE_TYPE: str = "memory"
REMOVE_API_KEYS: bool = False
COMPONENTS_PATH: List[str] = []
+ LANGCHAIN_CACHE: str = "InMemoryCache"
+
+ # Redis
+ REDIS_HOST: str = "localhost"
+ REDIS_PORT: int = 6379
+ REDIS_DB: int = 0
+ REDIS_CACHE_EXPIRE: int = 3600
LANGFUSE_SECRET_KEY: Optional[str] = None
LANGFUSE_PUBLIC_KEY: Optional[str] = None
diff --git a/src/backend/langflow/services/settings/factory.py b/src/backend/langflow/services/settings/factory.py
index ab22e22b8..9202ae8c3 100644
--- a/src/backend/langflow/services/settings/factory.py
+++ b/src/backend/langflow/services/settings/factory.py
@@ -1,15 +1,15 @@
from pathlib import Path
-from langflow.services.settings.manager import SettingsManager
+from langflow.services.settings.manager import SettingsService
from langflow.services.factory import ServiceFactory
-class SettingsManagerFactory(ServiceFactory):
+class SettingsServiceFactory(ServiceFactory):
def __init__(self):
- super().__init__(SettingsManager)
+ super().__init__(SettingsService)
def create(self):
- # Here you would have logic to create and configure a SettingsManager
+ # Here you would have logic to create and configure a SettingsService
langflow_dir = Path(__file__).parent.parent.parent
- return SettingsManager.load_settings_from_yaml(
+ return SettingsService.load_settings_from_yaml(
str(langflow_dir / "config.yaml")
)
diff --git a/src/backend/langflow/services/settings/manager.py b/src/backend/langflow/services/settings/manager.py
index 2d687d784..cdededcea 100644
--- a/src/backend/langflow/services/settings/manager.py
+++ b/src/backend/langflow/services/settings/manager.py
@@ -6,8 +6,8 @@ import os
import yaml
-class SettingsManager(Service):
- name = "settings_manager"
+class SettingsService(Service):
+ name = "settings_service"
def __init__(self, settings: Settings, auth_settings: AuthSettings):
super().__init__()
@@ -15,7 +15,7 @@ class SettingsManager(Service):
self.auth_settings = auth_settings
@classmethod
- def load_settings_from_yaml(cls, file_path: str) -> "SettingsManager":
+ def load_settings_from_yaml(cls, file_path: str) -> "SettingsService":
# Check if a string is a valid path or a file name
if "/" not in file_path:
# Get current path
diff --git a/src/backend/langflow/services/task/__init__.py b/src/backend/langflow/services/task/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/backend/langflow/services/task/backends/__init__.py b/src/backend/langflow/services/task/backends/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/backend/langflow/services/task/backends/anyio.py b/src/backend/langflow/services/task/backends/anyio.py
new file mode 100644
index 000000000..ca91eca19
--- /dev/null
+++ b/src/backend/langflow/services/task/backends/anyio.py
@@ -0,0 +1,67 @@
+from typing import Any, Callable, Optional, Tuple
+import anyio
+from langflow.services.task.backends.base import TaskBackend
+from loguru import logger
+
+
+class AnyIOTaskResult:
+ def __init__(self, scope):
+ self._scope = scope
+ self._status = "PENDING"
+ self._result = None
+ self._exception = None
+
+ @property
+ def status(self) -> str:
+ if self._status == "DONE":
+ return "FAILURE" if self._exception is not None else "SUCCESS"
+ return self._status
+
+ @property
+ def result(self) -> Any:
+ return self._result
+
+ def ready(self) -> bool:
+ return self._status == "DONE"
+
+ async def run(self, func, *args, **kwargs):
+ try:
+ self._result = await func(*args, **kwargs)
+ except Exception as e:
+ self._exception = e
+ finally:
+ self._status = "DONE"
+
+
+class AnyIOBackend(TaskBackend):
+ def __init__(self):
+ self.tasks = {}
+
+ async def launch_task(
+ self, task_func: Callable[..., Any], *args: Any, **kwargs: Any
+ ) -> Tuple[Optional[str], Optional[AnyIOTaskResult]]:
+ """
+ Launch a new task in an asynchronous manner.
+
+ Parameters:
+ task_func: The asynchronous function to run.
+ *args: Positional arguments to pass to task_func.
+ **kwargs: Keyword arguments to pass to task_func.
+
+ Returns:
+ A tuple containing a unique task ID and the task result object.
+ """
+ async with anyio.create_task_group() as tg:
+ try:
+ task_result = AnyIOTaskResult(tg)
+ tg.start_soon(task_result.run, task_func, *args, **kwargs)
+ task_id = str(id(task_result))
+ self.tasks[task_id] = task_result
+ logger.info(f"Task {task_id} started.")
+ return task_id, task_result
+ except Exception as e:
+ logger.error(f"An error occurred while launching the task: {e}")
+ return None, None
+
+ def get_task(self, task_id: str) -> Any:
+ return self.tasks.get(task_id)
diff --git a/src/backend/langflow/services/task/backends/base.py b/src/backend/langflow/services/task/backends/base.py
new file mode 100644
index 000000000..ccbd9273b
--- /dev/null
+++ b/src/backend/langflow/services/task/backends/base.py
@@ -0,0 +1,12 @@
+from abc import ABC, abstractmethod
+from typing import Any, Callable
+
+
+class TaskBackend(ABC):
+ @abstractmethod
+ def launch_task(self, task_func: Callable[..., Any], *args: Any, **kwargs: Any):
+ pass
+
+ @abstractmethod
+ def get_task(self, task_id: str) -> Any:
+ pass
diff --git a/src/backend/langflow/services/task/backends/celery.py b/src/backend/langflow/services/task/backends/celery.py
new file mode 100644
index 000000000..5b0a035ef
--- /dev/null
+++ b/src/backend/langflow/services/task/backends/celery.py
@@ -0,0 +1,23 @@
+from typing import Any, Callable
+from celery.result import AsyncResult # type: ignore
+from langflow.services.task.backends.base import TaskBackend
+from langflow.worker import celery_app
+
+
+class CeleryBackend(TaskBackend):
+ def __init__(self):
+ self.celery_app = celery_app
+
+ def launch_task(
+ self, task_func: Callable[..., Any], *args: Any, **kwargs: Any
+ ) -> tuple[str, AsyncResult]:
+ # I need to type the delay method to make it easier
+ from celery import Task # type: ignore
+
+ if not hasattr(task_func, "delay"):
+ raise ValueError(f"Task function {task_func} does not have a delay method")
+ task: Task = task_func.delay(*args, **kwargs)
+ return task.id, AsyncResult(task.id, app=self.celery_app)
+
+ def get_task(self, task_id: str) -> Any:
+ return AsyncResult(task_id, app=self.celery_app)
diff --git a/src/backend/langflow/services/task/factory.py b/src/backend/langflow/services/task/factory.py
new file mode 100644
index 000000000..efb6ac24d
--- /dev/null
+++ b/src/backend/langflow/services/task/factory.py
@@ -0,0 +1,11 @@
+from langflow.services.task.manager import TaskService
+from langflow.services.factory import ServiceFactory
+
+
+class TaskServiceFactory(ServiceFactory):
+ def __init__(self):
+ super().__init__(TaskService)
+
+ def create(self):
+ # Here you would have logic to create and configure a TaskService
+ return TaskService()
diff --git a/src/backend/langflow/services/task/manager.py b/src/backend/langflow/services/task/manager.py
new file mode 100644
index 000000000..ab74d916e
--- /dev/null
+++ b/src/backend/langflow/services/task/manager.py
@@ -0,0 +1,69 @@
+from typing import Any, Callable, Coroutine, Union
+from loguru import logger
+from langflow.services.base import Service
+from langflow.services.task.backends.anyio import AnyIOBackend
+from langflow.services.task.backends.base import TaskBackend
+from langflow.services.task.utils import get_celery_worker_status
+
+
+def check_celery_availability():
+ from langflow.worker import celery_app
+
+ try:
+ status = get_celery_worker_status(celery_app)
+ logger.debug(f"Celery status: {status}")
+ except Exception as e:
+ logger.error(f"An error occurred: {e}")
+ status = {"availability": None}
+ return status
+
+
+try:
+ status = check_celery_availability()
+
+ USE_CELERY = status.get("availability") is not None
+except ImportError:
+ USE_CELERY = False
+
+
+class TaskService(Service):
+ name = "task_service"
+
+ def __init__(self):
+ self.backend = self.get_backend()
+ self.use_celery = USE_CELERY
+
+ def get_backend(self) -> TaskBackend:
+ if USE_CELERY:
+ from langflow.services.task.backends.celery import CeleryBackend
+
+ logger.debug("Using Celery backend")
+ return CeleryBackend()
+ logger.debug("Using AnyIO backend")
+ return AnyIOBackend()
+
+ # In your TaskService class
+ async def launch_and_await_task(
+ self,
+ task_func: Callable[..., Any],
+ *args: Any,
+ **kwargs: Any,
+ ) -> Any:
+ if not self.use_celery:
+ return None, await task_func(*args, **kwargs)
+ if not hasattr(task_func, "apply"):
+ raise ValueError(f"Task function {task_func} does not have an apply method")
+ task = task_func.apply(args=args, kwargs=kwargs)
+ result = task.get()
+ return task.id, result
+
+ async def launch_task(
+ self, task_func: Callable[..., Any], *args: Any, **kwargs: Any
+ ) -> Any:
+ logger.debug(f"Launching task {task_func} with args {args} and kwargs {kwargs}")
+ logger.debug(f"Using backend {self.backend}")
+ task = self.backend.launch_task(task_func, *args, **kwargs)
+ return await task if isinstance(task, Coroutine) else task
+
+ def get_task(self, task_id: Union[int, str]) -> Any:
+ return self.backend.get_task(task_id)
diff --git a/src/backend/langflow/services/task/utils.py b/src/backend/langflow/services/task/utils.py
new file mode 100644
index 000000000..5dfb03b83
--- /dev/null
+++ b/src/backend/langflow/services/task/utils.py
@@ -0,0 +1,22 @@
+import contextlib
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ with contextlib.suppress(ImportError):
+ from celery import Celery # type: ignore
+
+
+def get_celery_worker_status(app: "Celery"):
+ i = app.control.inspect()
+ availability = app.control.ping()
+ stats = i.stats()
+ registered_tasks = i.registered()
+ active_tasks = i.active()
+ scheduled_tasks = i.scheduled()
+ return {
+ "availability": availability,
+ "stats": stats,
+ "registered_tasks": registered_tasks,
+ "active_tasks": active_tasks,
+ "scheduled_tasks": scheduled_tasks,
+ }
diff --git a/src/backend/langflow/services/utils.py b/src/backend/langflow/services/utils.py
index 5f8525797..02d9816f5 100644
--- a/src/backend/langflow/services/utils.py
+++ b/src/backend/langflow/services/utils.py
@@ -6,24 +6,22 @@ from langflow.services.settings.constants import (
DEFAULT_SUPERUSER,
DEFAULT_SUPERUSER_PASSWORD,
)
-from .getters import get_session, get_settings_manager
+from .getters import get_session, get_settings_service
from loguru import logger
-def setup_superuser():
+def setup_superuser(settings_service, session):
"""
Setup the superuser.
"""
# We will use the SUPERUSER and SUPERUSER_PASSWORD
# vars on settings_manager.auth_settings to create the superuser
# if it does not exist.
- settings_manager = get_settings_manager()
- if settings_manager.auth_settings.AUTO_LOGIN:
+ if settings_service.auth_settings.AUTO_LOGIN:
logger.debug("AUTO_LOGIN is set to True. Creating default superuser.")
- session = next(get_session())
- username = settings_manager.auth_settings.SUPERUSER
- password = settings_manager.auth_settings.SUPERUSER_PASSWORD
+ username = settings_service.auth_settings.SUPERUSER
+ password = settings_service.auth_settings.SUPERUSER_PASSWORD
if username == DEFAULT_SUPERUSER and password == DEFAULT_SUPERUSER_PASSWORD:
logger.debug("Default superuser credentials detected.")
logger.debug("Creating default superuser.")
@@ -50,21 +48,20 @@ def setup_superuser():
"Could not create superuser. Please create a superuser manually."
) from exc
# reset superuser credentials
- settings_manager.auth_settings.reset_credentials()
+ settings_service.auth_settings.reset_credentials()
logger.debug("Superuser created successfully.")
-def teardown_superuser():
+def teardown_superuser(settings_service, session):
"""
Teardown the superuser.
"""
# If AUTO_LOGIN is True, we will remove the default superuser
# from the database.
- settings_manager = get_settings_manager()
- if settings_manager.auth_settings.AUTO_LOGIN:
+
+ if settings_service.auth_settings.AUTO_LOGIN:
logger.debug("AUTO_LOGIN is set to True. Removing default superuser.")
- session = next(get_session())
- username = settings_manager.auth_settings.SUPERUSER
+ username = settings_service.auth_settings.SUPERUSER
from langflow.services.database.models.user.user import User
user = session.query(User).filter(User.username == username).first()
@@ -80,35 +77,38 @@ def teardown_services():
"""
Teardown all the services.
"""
- teardown_superuser()
- service_manager.teardown()
+ try:
+ teardown_superuser(get_settings_service(), next(get_session()))
+ service_manager.teardown()
+ except Exception as exc:
+ logger.exception(exc)
-def initialize_settings_manager():
+def initialize_settings_service():
"""
Initialize the settings manager.
"""
from langflow.services.settings import factory as settings_factory
- service_manager.register_factory(settings_factory.SettingsManagerFactory())
+ service_manager.register_factory(settings_factory.SettingsServiceFactory())
-def initialize_session_manager():
+def initialize_session_service():
"""
Initialize the session manager.
"""
- from langflow.services.session import factory as session_manager_factory # type: ignore
+ from langflow.services.session import factory as session_service_factory # type: ignore
from langflow.services.cache import factory as cache_factory
- initialize_settings_manager()
+ initialize_settings_service()
service_manager.register_factory(
- cache_factory.CacheManagerFactory(), dependencies=[ServiceType.SETTINGS_MANAGER]
+ cache_factory.CacheServiceFactory(), dependencies=[ServiceType.SETTINGS_SERVICE]
)
service_manager.register_factory(
- session_manager_factory.SessionManagerFactory(),
- dependencies=[ServiceType.CACHE_MANAGER],
+ session_service_factory.SessionServiceFactory(),
+ dependencies=[ServiceType.CACHE_SERVICE],
)
@@ -121,23 +121,33 @@ def initialize_services():
from langflow.services.chat import factory as chat_factory
from langflow.services.settings import factory as settings_factory
from langflow.services.auth import factory as auth_factory
+ from langflow.services.task import factory as task_factory
+ from langflow.services.session import factory as session_service_factory # type: ignore
- service_manager.register_factory(settings_factory.SettingsManagerFactory())
+ service_manager.register_factory(settings_factory.SettingsServiceFactory())
service_manager.register_factory(
- auth_factory.AuthManagerFactory(), dependencies=[ServiceType.SETTINGS_MANAGER]
+ auth_factory.AuthServiceFactory(), dependencies=[ServiceType.SETTINGS_SERVICE]
)
service_manager.register_factory(
- database_factory.DatabaseManagerFactory(),
- dependencies=[ServiceType.SETTINGS_MANAGER],
+ database_factory.DatabaseServiceFactory(),
+ dependencies=[ServiceType.SETTINGS_SERVICE],
)
- service_manager.register_factory(cache_factory.CacheManagerFactory())
- service_manager.register_factory(chat_factory.ChatManagerFactory())
+ service_manager.register_factory(
+ cache_factory.CacheServiceFactory(), dependencies=[ServiceType.SETTINGS_SERVICE]
+ )
+ service_manager.register_factory(chat_factory.ChatServiceFactory())
+ service_manager.register_factory(task_factory.TaskServiceFactory())
+
+ service_manager.register_factory(
+ session_service_factory.SessionServiceFactory(),
+ dependencies=[ServiceType.CACHE_SERVICE],
+ )
# Test cache connection
- service_manager.get(ServiceType.CACHE_MANAGER)
+ service_manager.get(ServiceType.CACHE_SERVICE)
# Test database connection
- db_manager = service_manager.get(ServiceType.DATABASE_MANAGER)
+ service_manager.get(ServiceType.DATABASE_SERVICE)
# Setup the superuser
initialize_database()
- if db_manager.ready:
- setup_superuser()
+ session = next(get_session())
+ setup_superuser(service_manager.get(ServiceType.SETTINGS_SERVICE), session)
diff --git a/src/backend/langflow/utils/constants.py b/src/backend/langflow/utils/constants.py
index 43f92f651..0c97b56a2 100644
--- a/src/backend/langflow/utils/constants.py
+++ b/src/backend/langflow/utils/constants.py
@@ -48,6 +48,8 @@ def python_function(text: str) -> str:
return text
"""
+
+PYTHON_BASIC_TYPES = [str, bool, int, float, tuple, list, dict, set]
DIRECT_TYPES = [
"str",
"bool",
diff --git a/src/backend/langflow/worker.py b/src/backend/langflow/worker.py
new file mode 100644
index 000000000..2eeba14a5
--- /dev/null
+++ b/src/backend/langflow/worker.py
@@ -0,0 +1,62 @@
+from langflow.core.celery_app import celery_app
+from typing import Any, Dict, Optional
+from typing import TYPE_CHECKING
+
+from celery.exceptions import SoftTimeLimitExceeded # type: ignore
+from langflow.processing.process import (
+ Result,
+ generate_result,
+ process_inputs,
+)
+from langflow.services.manager import initialize_session_service
+from langflow.services.getters import get_session_service
+
+if TYPE_CHECKING:
+ from langflow.graph.vertex.base import Vertex
+
+
+@celery_app.task(acks_late=True)
+def test_celery(word: str) -> str:
+ return f"test task return {word}"
+
+
+@celery_app.task(bind=True, soft_time_limit=30, max_retries=3)
+def build_vertex(self, vertex: "Vertex") -> "Vertex":
+ """
+ Build a vertex
+ """
+ try:
+ vertex.task_id = self.request.id
+ vertex.build()
+ return vertex
+ except SoftTimeLimitExceeded as e:
+ raise self.retry(
+ exc=SoftTimeLimitExceeded("Task took too long"), countdown=2
+ ) from e
+
+
+@celery_app.task(acks_late=True)
+def process_graph_cached_task(
+ data_graph: Dict[str, Any],
+ inputs: Optional[dict] = None,
+ clear_cache=False,
+ session_id=None,
+) -> Dict[str, Any]:
+ initialize_session_service()
+ 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(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(result=result, session_id=session_id).dict()
diff --git a/src/frontend/Dockerfile b/src/frontend/Dockerfile
index 010811e7b..024eab562 100644
--- a/src/frontend/Dockerfile
+++ b/src/frontend/Dockerfile
@@ -1,10 +1,16 @@
-FROM node:14-alpine as frontend_build
-ARG BACKEND
+FROM node:20-alpine as frontend_build
+ARG BACKEND_URL
WORKDIR /app
-COPY . /app
+
+COPY ./package.json ./package-lock.json ./tsconfig.json ./vite.config.ts ./index.html ./tailwind.config.js ./postcss.config.js ./prettier.config.js /app/
RUN npm install
+COPY ./src /app/src
RUN npm run build
FROM nginx
COPY --from=frontend_build /app/build/ /usr/share/nginx/html
-COPY /nginx.conf /etc/nginx/conf.d/default.conf
\ No newline at end of file
+COPY /nginx.conf /etc/nginx/conf.d/default.conf
+COPY start-nginx.sh /start-nginx.sh
+RUN chmod +x /start-nginx.sh
+ENV BACKEND_URL=$BACKEND_URL
+CMD ["/start-nginx.sh"]
\ No newline at end of file
diff --git a/src/frontend/nginx.conf b/src/frontend/nginx.conf
index df665f5b6..cabf31c7a 100644
--- a/src/frontend/nginx.conf
+++ b/src/frontend/nginx.conf
@@ -1,18 +1,20 @@
server {
- gzip on;
- gzip_comp_level 2;
- gzip_min_length 1000;
- gzip_types text/xml text/css;
- gzip_http_version 1.1;
- gzip_vary on;
- gzip_disable "MSIE [4-6] \.";
+ gzip on;
+ gzip_comp_level 2;
+ gzip_min_length 1000;
+ gzip_types text/xml text/css;
+ gzip_http_version 1.1;
+ gzip_vary on;
+ gzip_disable "MSIE [4-6] \.";
- listen 80;
+ listen 80;
- location / {
- root /usr/share/nginx/html;
- index index.html index.htm;
- try_files $uri $uri/ /index.html =404;
- }
-
+ location / {
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+ try_files $uri $uri/ /index.html =404;
+ }
+
+
+ include /etc/nginx/extra-conf.d/*.conf;
}
diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json
index 1ff16d6d0..c4a0660cb 100644
--- a/src/frontend/package-lock.json
+++ b/src/frontend/package-lock.json
@@ -710,51 +710,6 @@
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
"integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
},
- "node_modules/@esbuild/android-arm": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
- "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
- "cpu": [
- "arm"
- ],
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
- "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
- "cpu": [
- "arm64"
- ],
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
- "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/@esbuild/darwin-arm64": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
@@ -770,276 +725,6 @@
"node": ">=12"
}
},
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
- "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
- "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
- "cpu": [
- "arm64"
- ],
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
- "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
- "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
- "cpu": [
- "arm"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
- "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
- "cpu": [
- "arm64"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
- "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
- "cpu": [
- "ia32"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
- "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
- "cpu": [
- "loong64"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
- "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
- "cpu": [
- "mips64el"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
- "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
- "cpu": [
- "ppc64"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
- "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
- "cpu": [
- "riscv64"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
- "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
- "cpu": [
- "s390x"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
- "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
- "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
- "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
- "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
- "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
- "cpu": [
- "arm64"
- ],
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
- "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
- "cpu": [
- "ia32"
- ],
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.17.19",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
- "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/@floating-ui/core": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz",
diff --git a/src/frontend/package.json b/src/frontend/package.json
index 36cf0d25c..1c64b6333 100644
--- a/src/frontend/package.json
+++ b/src/frontend/package.json
@@ -73,7 +73,7 @@
"start": "vite",
"build": "vite build",
"serve": "vite preview",
- "format": "npx prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
+ "format": "npx prettier --write \"./**/*.{js,jsx,ts,tsx,json,md}\"",
"type-check": "tsc --noEmit --pretty --project tsconfig.json && vite"
},
"eslintConfig": {
diff --git a/src/frontend/src/pages/AdminPage/index.tsx b/src/frontend/src/pages/AdminPage/index.tsx
index 24baefb5b..f6e17a32a 100644
--- a/src/frontend/src/pages/AdminPage/index.tsx
+++ b/src/frontend/src/pages/AdminPage/index.tsx
@@ -63,7 +63,7 @@ export default function AdminPage() {
function getUsers() {
setLoadingUsers(true);
- getUsersPage(index, size)
+ getUsersPage(index - 1, size)
.then((users) => {
setTotalRowsCount(users["total_count"]);
userList.current = users["users"];
diff --git a/src/frontend/start-nginx.sh b/src/frontend/start-nginx.sh
new file mode 100644
index 000000000..dd20f5305
--- /dev/null
+++ b/src/frontend/start-nginx.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+# Replace the placeholder with the actual value
+sed -i "s|__BACKEND_URL__|$BACKEND_URL|g" /etc/nginx/conf.d/default.conf
+
+
+# Start nginx
+exec nginx -g 'daemon off;'
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/conftest.py b/tests/conftest.py
index 2c8b9016e..c58f35cf5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,14 +1,16 @@
from contextlib import contextmanager
import json
+from contextlib import suppress
from pathlib import Path
from typing import AsyncGenerator, TYPE_CHECKING
from langflow.graph.graph.base import Graph
from langflow.services.auth.utils import get_password_hash
-from langflow.services.database.models.flow.flow import Flow
+from langflow.services.database.models.flow.flow import Flow, FlowCreate
from langflow.services.database.models.user.user import User, UserCreate
+import orjson
from langflow.services.database.utils import session_getter
-from langflow.services.getters import get_db_manager
+from langflow.services.getters import get_db_service
import pytest
from fastapi.testclient import TestClient
from httpx import AsyncClient
@@ -20,7 +22,7 @@ from typer.testing import CliRunner
import tempfile
if TYPE_CHECKING:
- from langflow.services.database.manager import DatabaseManager
+ from langflow.services.database.manager import DatabaseService
def pytest_configure():
@@ -33,7 +35,12 @@ def pytest_configure():
pytest.OPENAPI_EXAMPLE_PATH = (
Path(__file__).parent.absolute() / "data" / "Openapi.json"
)
-
+ pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY = (
+ Path(__file__).parent.absolute() / "data" / "BasicChatwithPromptandHistory.json"
+ )
+ pytest.VECTOR_STORE_PATH = (
+ Path(__file__).parent.absolute() / "data" / "Vector_store.json"
+ )
pytest.CODE_WITH_SYNTAX_ERROR = """
def get_text():
retun "Hello World"
@@ -49,15 +56,62 @@ async def async_client() -> AsyncGenerator:
yield client
-# Create client fixture for FastAPI
-# @pytest.fixture(scope="module", autouse=True)
-# def client():
-# from langflow.main import create_app
+@pytest.fixture(name="session")
+def session_fixture():
+ engine = create_engine(
+ "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool
+ )
+ SQLModel.metadata.create_all(engine)
+ with Session(engine) as session:
+ yield session
-# app = create_app()
-# with TestClient(app) as client:
-# yield client
+class Config:
+ broker_url = "redis://localhost:6379/0"
+ result_backend = "redis://localhost:6379/0"
+
+
+@pytest.fixture(name="distributed_env")
+def setup_env(monkeypatch):
+ monkeypatch.setenv("LANGFLOW_CACHE_TYPE", "redis")
+ monkeypatch.setenv("LANGFLOW_REDIS_HOST", "result_backend")
+ monkeypatch.setenv("LANGFLOW_REDIS_PORT", "6379")
+ monkeypatch.setenv("LANGFLOW_REDIS_DB", "0")
+ monkeypatch.setenv("LANGFLOW_REDIS_EXPIRE", "3600")
+ monkeypatch.setenv("LANGFLOW_REDIS_PASSWORD", "")
+ monkeypatch.setenv("FLOWER_UNAUTHENTICATED_API", "True")
+ monkeypatch.setenv("BROKER_URL", "redis://result_backend:6379/0")
+ monkeypatch.setenv("RESULT_BACKEND", "redis://result_backend:6379/0")
+ monkeypatch.setenv("C_FORCE_ROOT", "true")
+
+
+@pytest.fixture(name="distributed_client")
+def distributed_client_fixture(session: Session, monkeypatch, distributed_env):
+ # Here we load the .env from ../deploy/.env
+ from langflow.core import celery_app
+
+ db_dir = tempfile.mkdtemp()
+ db_path = Path(db_dir) / "test.db"
+ monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
+ monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false")
+ # monkeypatch langflow.services.task.manager.USE_CELERY to True
+ # monkeypatch.setattr(manager, "USE_CELERY", True)
+ monkeypatch.setattr(
+ celery_app, "celery_app", celery_app.make_celery("langflow", Config)
+ )
+
+ # def get_session_override():
+ # return session
+
+ from langflow.main import create_app
+
+ app = create_app()
+
+ # app.dependency_overrides[get_session] = get_session_override
+ with TestClient(app) as client:
+ yield client
+ app.dependency_overrides.clear()
+ monkeypatch.undo()
def get_graph(_type="basic"):
@@ -105,14 +159,16 @@ def json_flow():
return f.read()
-@pytest.fixture(name="session")
-def session_fixture():
- engine = create_engine(
- "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool
- )
- SQLModel.metadata.create_all(engine)
- with Session(engine) as session:
- yield session
+@pytest.fixture
+def json_flow_with_prompt_and_history():
+ with open(pytest.BASIC_CHAT_WITH_PROMPT_AND_HISTORY, "r") as f:
+ return f.read()
+
+
+@pytest.fixture
+def json_vector_store():
+ with open(pytest.VECTOR_STORE_PATH, "r") as f:
+ return f.read()
@pytest.fixture(name="client", autouse=True)
@@ -121,10 +177,7 @@ def client_fixture(session: Session, monkeypatch):
db_dir = tempfile.mkdtemp()
db_path = Path(db_dir) / "test.db"
monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
- # monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", 1)
-
- def get_session_override():
- return session
+ monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false")
from langflow.main import create_app
@@ -136,28 +189,16 @@ def client_fixture(session: Session, monkeypatch):
# app.dependency_overrides.clear()
monkeypatch.undo()
# clear the temp db
- db_path.unlink()
-
-
-# @contextmanager
-# def session_getter():
-# try:
-# session = Session(engine)
-# yield session
-# except Exception as e:
-# print("Session rollback because of exception:", e)
-# session.rollback()
-# raise
-# finally:
-# session.close()
+ with suppress(FileNotFoundError):
+ db_path.unlink()
# create a fixture for session_getter above
@pytest.fixture(name="session_getter")
def session_getter_fixture(client):
@contextmanager
- def blank_session_getter(db_manager: "DatabaseManager"):
- with Session(db_manager.engine) as session:
+ def blank_session_getter(db_service: "DatabaseService"):
+ with Session(db_service.engine) as session:
yield session
yield blank_session_getter
@@ -181,7 +222,7 @@ def test_user(client):
@pytest.fixture(scope="function")
def active_user(client):
- db_manager = get_db_manager()
+ db_manager = get_db_service()
with session_getter(db_manager) as session:
user = User(
username="activeuser",
@@ -189,6 +230,13 @@ def active_user(client):
is_active=True,
is_superuser=False,
)
+ # check if user exists
+ if (
+ active_user := session.query(User)
+ .filter(User.username == user.username)
+ .first()
+ ):
+ return active_user
session.add(user)
session.commit()
session.refresh(user)
@@ -214,9 +262,35 @@ def flow(client, json_flow: str, active_user):
name="test_flow", data=loaded_json.get("data"), user_id=active_user.id
)
flow = Flow(**flow_data.dict())
- with session_getter(get_db_manager()) as session:
+ with session_getter(get_db_service()) as session:
session.add(flow)
session.commit()
session.refresh(flow)
return flow
+
+
+@pytest.fixture
+def added_flow(client, json_flow_with_prompt_and_history, logged_in_headers):
+ flow = orjson.loads(json_flow_with_prompt_and_history)
+ data = flow["data"]
+ flow = FlowCreate(name="Basic Chat", description="description", data=data)
+ response = client.post("api/v1/flows/", json=flow.dict(), headers=logged_in_headers)
+ assert response.status_code == 201
+ assert response.json()["name"] == flow.name
+ assert response.json()["data"] == flow.data
+ return response.json()
+
+
+@pytest.fixture
+def added_vector_store(client, json_vector_store, logged_in_headers):
+ vector_store = orjson.loads(json_vector_store)
+ data = vector_store["data"]
+ vector_store = FlowCreate(name="Vector Store", description="description", data=data)
+ response = client.post(
+ "api/v1/flows/", json=vector_store.dict(), headers=logged_in_headers
+ )
+ assert response.status_code == 201
+ assert response.json()["name"] == vector_store.name
+ assert response.json()["data"] == vector_store.data
+ return response.json()
diff --git a/tests/data/BasicChatwithPromptandHistory.json b/tests/data/BasicChatwithPromptandHistory.json
new file mode 100644
index 000000000..658ac0479
--- /dev/null
+++ b/tests/data/BasicChatwithPromptandHistory.json
@@ -0,0 +1 @@
+{"description":"A simple chat with a custom prompt template and conversational memory buffer","name":"Basic Chat with Prompt and History (2)","data":{"nodes":[{"width":384,"height":621,"id":"ChatOpenAI-vy7fV","type":"genericNode","position":{"x":170.87326389541306,"y":465.8628482073749},"data":{"type":"ChatOpenAI","node":{"template":{"callbacks":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"callbacks","advanced":false,"dynamic":false,"info":"","type":"langchain.callbacks.base.BaseCallbackHandler","list":true},"cache":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"cache","advanced":false,"dynamic":false,"info":"","type":"bool","list":false},"client":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"client","advanced":false,"dynamic":false,"info":"","type":"Any","list":false},"max_retries":{"required":false,"placeholder":"","show":false,"multiline":false,"value":6,"password":false,"name":"max_retries","advanced":false,"dynamic":false,"info":"","type":"int","list":false},"max_tokens":{"required":false,"placeholder":"","show":true,"multiline":false,"password":true,"name":"max_tokens","advanced":false,"dynamic":false,"info":"","type":"int","list":false,"value":""},"metadata":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"metadata","advanced":false,"dynamic":false,"info":"","type":"code","list":false},"model_kwargs":{"required":false,"placeholder":"","show":true,"multiline":false,"password":false,"name":"model_kwargs","advanced":true,"dynamic":false,"info":"","type":"code","list":false},"model_name":{"required":false,"placeholder":"","show":true,"multiline":false,"value":"gpt-3.5-turbo","password":false,"options":["gpt-3.5-turbo-0613","gpt-3.5-turbo","gpt-3.5-turbo-16k-0613","gpt-3.5-turbo-16k","gpt-4-0613","gpt-4-32k-0613","gpt-4","gpt-4-32k"],"name":"model_name","advanced":false,"dynamic":false,"info":"","type":"str","list":true},"n":{"required":false,"placeholder":"","show":false,"multiline":false,"value":1,"password":false,"name":"n","advanced":false,"dynamic":false,"info":"","type":"int","list":false},"openai_api_base":{"required":false,"placeholder":"","show":true,"multiline":false,"password":false,"name":"openai_api_base","display_name":"OpenAI API Base","advanced":false,"dynamic":false,"info":"\nThe base URL of the OpenAI API. Defaults to https://api.openai.com/v1.\n\nYou can change this to use other APIs like JinaChat, LocalAI and Prem.\n","type":"str","list":false},"openai_api_key":{"required":false,"placeholder":"","show":true,"multiline":false,"value":"","password":true,"name":"openai_api_key","display_name":"OpenAI API Key","advanced":false,"dynamic":false,"info":"","type":"str","list":false},"openai_organization":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"openai_organization","display_name":"OpenAI Organization","advanced":false,"dynamic":false,"info":"","type":"str","list":false},"openai_proxy":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"openai_proxy","display_name":"OpenAI Proxy","advanced":false,"dynamic":false,"info":"","type":"str","list":false},"request_timeout":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"request_timeout","advanced":false,"dynamic":false,"info":"","type":"float","list":false,"value":60},"streaming":{"required":false,"placeholder":"","show":false,"multiline":false,"value":false,"password":false,"name":"streaming","advanced":false,"dynamic":false,"info":"","type":"bool","list":false},"tags":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"tags","advanced":false,"dynamic":false,"info":"","type":"str","list":true},"temperature":{"required":false,"placeholder":"","show":true,"multiline":false,"value":0.7,"password":false,"name":"temperature","advanced":false,"dynamic":false,"info":"","type":"float","list":false},"tiktoken_model_name":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"tiktoken_model_name","advanced":false,"dynamic":false,"info":"","type":"str","list":false},"verbose":{"required":false,"placeholder":"","show":false,"multiline":false,"value":false,"password":false,"name":"verbose","advanced":false,"dynamic":false,"info":"","type":"bool","list":false},"_type":"ChatOpenAI"},"description":"`OpenAI` Chat large language models API.","base_classes":["ChatOpenAI","BaseChatModel","BaseLanguageModel","BaseLLM"],"display_name":"ChatOpenAI","documentation":"https://python.langchain.com/docs/modules/model_io/models/chat/integrations/openai"},"id":"ChatOpenAI-vy7fV","value":null},"selected":true,"dragging":false,"positionAbsolute":{"x":170.87326389541306,"y":465.8628482073749}},{"width":384,"height":307,"id":"LLMChain-UjBh1","type":"genericNode","position":{"x":1250.1806448178158,"y":588.4657451068704},"data":{"type":"LLMChain","node":{"template":{"callbacks":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"callbacks","advanced":false,"dynamic":false,"info":"","type":"langchain.callbacks.base.BaseCallbackHandler","list":true},"llm":{"required":true,"placeholder":"","show":true,"multiline":false,"password":false,"name":"llm","advanced":false,"dynamic":false,"info":"","type":"BaseLanguageModel","list":false},"memory":{"required":false,"placeholder":"","show":true,"multiline":false,"password":false,"name":"memory","advanced":false,"dynamic":false,"info":"","type":"BaseMemory","list":false},"output_parser":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"output_parser","advanced":false,"dynamic":false,"info":"","type":"BaseLLMOutputParser","list":false},"prompt":{"required":true,"placeholder":"","show":true,"multiline":false,"password":false,"name":"prompt","advanced":false,"dynamic":false,"info":"","type":"BasePromptTemplate","list":false},"llm_kwargs":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"llm_kwargs","advanced":false,"dynamic":false,"info":"","type":"code","list":false},"metadata":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"metadata","advanced":false,"dynamic":false,"info":"","type":"code","list":false},"output_key":{"required":true,"placeholder":"","show":true,"multiline":false,"value":"text","password":false,"name":"output_key","advanced":true,"dynamic":false,"info":"","type":"str","list":false},"return_final_only":{"required":false,"placeholder":"","show":false,"multiline":false,"value":true,"password":false,"name":"return_final_only","advanced":false,"dynamic":false,"info":"","type":"bool","list":false},"tags":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"tags","advanced":false,"dynamic":false,"info":"","type":"str","list":true},"verbose":{"required":false,"placeholder":"","show":false,"multiline":false,"value":false,"password":false,"name":"verbose","advanced":true,"dynamic":false,"info":"","type":"bool","list":false},"_type":"LLMChain"},"description":"Chain to run queries against LLMs.","base_classes":["LLMChain","Chain","function"],"display_name":"LLMChain","documentation":"https://python.langchain.com/docs/modules/chains/foundational/llm_chain"},"id":"LLMChain-UjBh1","value":null},"selected":false,"positionAbsolute":{"x":1250.1806448178158,"y":588.4657451068704},"dragging":false},{"width":384,"height":273,"id":"PromptTemplate-5Q0W8","type":"genericNode","position":{"x":172.18064481781585,"y":67.26574510687044},"data":{"type":"PromptTemplate","node":{"template":{"output_parser":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"output_parser","advanced":false,"dynamic":false,"info":"","type":"BaseOutputParser","list":false},"input_variables":{"required":true,"placeholder":"","show":false,"multiline":false,"password":false,"name":"input_variables","advanced":false,"dynamic":false,"info":"","type":"str","list":true,"value":["history","text"]},"partial_variables":{"required":false,"placeholder":"","show":false,"multiline":false,"password":false,"name":"partial_variables","advanced":false,"dynamic":false,"info":"","type":"code","list":false},"template":{"required":true,"placeholder":"","show":true,"multiline":true,"password":false,"name":"template","advanced":false,"dynamic":false,"info":"","type":"prompt","list":false,"value":"The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.\n\nCurrent conversation:\n\n{history}\nHuman: {text}\nAI:"},"template_format":{"required":false,"placeholder":"","show":false,"multiline":false,"value":"f-string","password":false,"name":"template_format","advanced":false,"dynamic":false,"info":"","type":"str","list":false},"validate_template":{"required":false,"placeholder":"","show":false,"multiline":false,"value":true,"password":false,"name":"validate_template","advanced":false,"dynamic":false,"info":"","type":"bool","list":false},"_type":"PromptTemplate","history":{"required":false,"placeholder":"","show":true,"multiline":true,"value":"","password":false,"name":"history","display_name":"history","advanced":false,"input_types":["Document","BaseOutputParser"],"dynamic":false,"info":"","type":"str","list":false},"text":{"required":false,"placeholder":"","show":true,"multiline":true,"value":"","password":false,"name":"text","display_name":"text","advanced":false,"input_types":["Document","BaseOutputParser"],"dynamic":false,"info":"","type":"str","list":false}},"description":"A prompt template for a language model.","base_classes":["StringPromptTemplate","PromptTemplate","BasePromptTemplate"],"name":"","display_name":"PromptTemplate","documentation":"https://python.langchain.com/docs/modules/model_io/prompts/prompt_templates/","custom_fields":{"template":["history","text"]},"output_types":[],"field_formatters":{"formatters":{"openai_api_key":{}},"base_formatters":{"kwargs":{},"optional":{},"list":{},"dict":{},"union":{},"multiline":{},"show":{},"password":{},"default":{},"headers":{},"dict_code_file":{},"model_fields":{"MODEL_DICT":{"OpenAI":["text-davinci-003","text-davinci-002","text-curie-001","text-babbage-001","text-ada-001"],"ChatOpenAI":["gpt-3.5-turbo-0613","gpt-3.5-turbo","gpt-3.5-turbo-16k-0613","gpt-3.5-turbo-16k","gpt-4-0613","gpt-4-32k-0613","gpt-4","gpt-4-32k"],"Anthropic":["claude-v1","claude-v1-100k","claude-instant-v1","claude-instant-v1-100k","claude-v1.3","claude-v1.3-100k","claude-v1.2","claude-v1.0","claude-instant-v1.1","claude-instant-v1.1-100k","claude-instant-v1.0"],"ChatAnthropic":["claude-v1","claude-v1-100k","claude-instant-v1","claude-instant-v1-100k","claude-v1.3","claude-v1.3-100k","claude-v1.2","claude-v1.0","claude-instant-v1.1","claude-instant-v1.1-100k","claude-instant-v1.0"]}}}},"beta":false,"error":null},"id":"PromptTemplate-5Q0W8","value":null},"selected":false,"dragging":false,"positionAbsolute":{"x":172.18064481781585,"y":67.26574510687044}},{"width":384,"height":561,"id":"ConversationBufferMemory-Lu2Nb","type":"genericNode","position":{"x":802.1806448178158,"y":43.265745106870426},"data":{"type":"ConversationBufferMemory","node":{"template":{"chat_memory":{"required":false,"placeholder":"","show":true,"multiline":false,"password":false,"name":"chat_memory","advanced":false,"dynamic":false,"info":"","type":"BaseChatMessageHistory","list":false},"ai_prefix":{"required":false,"placeholder":"","show":false,"multiline":false,"value":"AI","password":false,"name":"ai_prefix","advanced":false,"dynamic":false,"info":"","type":"str","list":false},"human_prefix":{"required":false,"placeholder":"","show":false,"multiline":false,"value":"Human","password":false,"name":"human_prefix","advanced":false,"dynamic":false,"info":"","type":"str","list":false},"input_key":{"required":false,"placeholder":"","show":true,"multiline":false,"value":"","password":false,"name":"input_key","advanced":false,"dynamic":false,"info":"The variable to be used as Chat Input when more than one variable is available.","type":"str","list":false},"memory_key":{"required":false,"placeholder":"","show":true,"multiline":false,"value":"history","password":false,"name":"memory_key","advanced":false,"dynamic":false,"info":"","type":"str","list":false},"output_key":{"required":false,"placeholder":"","show":true,"multiline":false,"value":"","password":false,"name":"output_key","advanced":false,"dynamic":false,"info":"The variable to be used as Chat Output (e.g. answer in a ConversationalRetrievalChain)","type":"str","list":false},"return_messages":{"required":false,"placeholder":"","show":true,"multiline":false,"password":false,"name":"return_messages","advanced":false,"dynamic":false,"info":"","type":"bool","list":false},"_type":"ConversationBufferMemory"},"description":"Buffer for storing conversation memory.","base_classes":["BaseMemory","ConversationBufferMemory","BaseChatMemory"],"display_name":"ConversationBufferMemory","documentation":"https://python.langchain.com/docs/modules/memory/how_to/buffer"},"id":"ConversationBufferMemory-Lu2Nb","value":null},"selected":false,"positionAbsolute":{"x":802.1806448178158,"y":43.265745106870426},"dragging":false}],"edges":[{"source":"ChatOpenAI-vy7fV","sourceHandle":"ChatOpenAI|ChatOpenAI-vy7fV|ChatOpenAI|BaseChatModel|BaseLanguageModel|BaseLLM","target":"LLMChain-UjBh1","targetHandle":"BaseLanguageModel|llm|LLMChain-UjBh1","className":"","id":"reactflow__edge-ChatOpenAI-vy7fVChatOpenAI|ChatOpenAI-vy7fV|ChatOpenAI|BaseChatModel|BaseLanguageModel|BaseLLM-LLMChain-UjBh1BaseLanguageModel|llm|LLMChain-UjBh1","selected":false,"animated":false,"style":{"stroke":"#555"}},{"source":"PromptTemplate-5Q0W8","sourceHandle":"PromptTemplate|PromptTemplate-5Q0W8|StringPromptTemplate|PromptTemplate|BasePromptTemplate","target":"LLMChain-UjBh1","targetHandle":"BasePromptTemplate|prompt|LLMChain-UjBh1","className":"","id":"reactflow__edge-PromptTemplate-5Q0W8PromptTemplate|PromptTemplate-5Q0W8|StringPromptTemplate|PromptTemplate|BasePromptTemplate-LLMChain-UjBh1BasePromptTemplate|prompt|LLMChain-UjBh1","animated":false,"style":{"stroke":"#555"}},{"source":"ConversationBufferMemory-Lu2Nb","sourceHandle":"ConversationBufferMemory|ConversationBufferMemory-Lu2Nb|BaseMemory|ConversationBufferMemory|BaseChatMemory","target":"LLMChain-UjBh1","targetHandle":"BaseMemory|memory|LLMChain-UjBh1","className":"","id":"reactflow__edge-ConversationBufferMemory-Lu2NbConversationBufferMemory|ConversationBufferMemory-Lu2Nb|BaseMemory|ConversationBufferMemory|BaseChatMemory-LLMChain-UjBh1BaseMemory|memory|LLMChain-UjBh1","animated":false,"style":{"stroke":"#555"}}],"viewport":{"x":-64.70809474436828,"y":44.7801470275611,"zoom":0.6622606580990782}},"id":"0cdfb2f2-19de-4e15-99fa-fd5203b38053"}
\ No newline at end of file
diff --git a/tests/data/Vector_store.json b/tests/data/Vector_store.json
new file mode 100644
index 000000000..2a1ddd5f3
--- /dev/null
+++ b/tests/data/Vector_store.json
@@ -0,0 +1,1283 @@
+{
+ "name": "Vector Store",
+ "description": "An agent that can query a Vector Store.\nTry asking \"How do I upload examples to Langflow?\"\n\n\n\n",
+ "data": {
+ "nodes": [
+ {
+ "width": 384,
+ "height": 267,
+ "id": "VectorStoreAgent-FOmxY",
+ "type": "genericNode",
+ "position": {
+ "x": 2115.5183674856203,
+ "y": -1277.6284872455249
+ },
+ "data": {
+ "type": "VectorStoreAgent",
+ "node": {
+ "template": {
+ "llm": {
+ "required": true,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "llm",
+ "display_name": "LLM",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "BaseLanguageModel",
+ "list": false
+ },
+ "vectorstoreinfo": {
+ "required": true,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "vectorstoreinfo",
+ "display_name": "Vector Store Info",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "VectorStoreInfo",
+ "list": false
+ },
+ "_type": "vectorstore_agent"
+ },
+ "description": "Construct an agent from a Vector Store.",
+ "base_classes": [
+ "AgentExecutor"
+ ],
+ "display_name": "VectorStoreAgent",
+ "documentation": ""
+ },
+ "id": "VectorStoreAgent-FOmxY",
+ "value": null
+ },
+ "selected": false,
+ "positionAbsolute": {
+ "x": 2115.5183674856203,
+ "y": -1277.6284872455249
+ },
+ "dragging": false
+ },
+ {
+ "width": 384,
+ "height": 399,
+ "id": "VectorStoreInfo-z0sH5",
+ "type": "genericNode",
+ "position": {
+ "x": 1553.2875394928135,
+ "y": -1319.2113273706286
+ },
+ "data": {
+ "type": "VectorStoreInfo",
+ "node": {
+ "template": {
+ "vectorstore": {
+ "required": true,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "vectorstore",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "VectorStore",
+ "list": false
+ },
+ "description": {
+ "required": true,
+ "placeholder": "",
+ "show": true,
+ "multiline": true,
+ "password": false,
+ "name": "description",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false,
+ "value": "Instructions to upload examples to Langflow Community Examples"
+ },
+ "name": {
+ "required": true,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "name",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false,
+ "value": "UploadExamples"
+ },
+ "_type": "VectorStoreInfo"
+ },
+ "description": "Information about a VectorStore.",
+ "base_classes": [
+ "VectorStoreInfo"
+ ],
+ "display_name": "VectorStoreInfo",
+ "documentation": ""
+ },
+ "id": "VectorStoreInfo-z0sH5",
+ "value": null
+ },
+ "selected": false,
+ "positionAbsolute": {
+ "x": 1553.2875394928135,
+ "y": -1319.2113273706286
+ },
+ "dragging": false
+ },
+ {
+ "width": 384,
+ "height": 359,
+ "id": "OpenAIEmbeddings-lge2J",
+ "type": "genericNode",
+ "position": {
+ "x": 677.2699276778915,
+ "y": -734.4639958173494
+ },
+ "data": {
+ "type": "OpenAIEmbeddings",
+ "node": {
+ "template": {
+ "allowed_special": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": [],
+ "password": false,
+ "name": "allowed_special",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "Literal'all'",
+ "list": true
+ },
+ "disallowed_special": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": "all",
+ "password": false,
+ "name": "disallowed_special",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "Literal'all'",
+ "list": true
+ },
+ "chunk_size": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": 1000,
+ "password": false,
+ "name": "chunk_size",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "int",
+ "list": false
+ },
+ "client": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "client",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "Any",
+ "list": false
+ },
+ "deployment": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": "text-embedding-ada-002",
+ "password": false,
+ "name": "deployment",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "embedding_ctx_length": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": 8191,
+ "password": false,
+ "name": "embedding_ctx_length",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "int",
+ "list": false
+ },
+ "headers": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": true,
+ "value": "{'Authorization':\n 'Bearer '}",
+ "password": false,
+ "name": "headers",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "Any",
+ "list": false
+ },
+ "max_retries": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": 6,
+ "password": false,
+ "name": "max_retries",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "int",
+ "list": false
+ },
+ "model": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": "text-embedding-ada-002",
+ "password": false,
+ "name": "model",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "model_kwargs": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "model_kwargs",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "code",
+ "list": false
+ },
+ "openai_api_base": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": true,
+ "name": "openai_api_base",
+ "display_name": "OpenAI API Base",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false,
+ "value": ""
+ },
+ "openai_api_key": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": "",
+ "password": true,
+ "name": "openai_api_key",
+ "display_name": "OpenAI API Key",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "openai_api_type": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": true,
+ "name": "openai_api_type",
+ "display_name": "OpenAI API Type",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false,
+ "value": ""
+ },
+ "openai_api_version": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": true,
+ "name": "openai_api_version",
+ "display_name": "OpenAI API Version",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false,
+ "value": ""
+ },
+ "openai_organization": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "openai_organization",
+ "display_name": "OpenAI Organization",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "openai_proxy": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "openai_proxy",
+ "display_name": "OpenAI Proxy",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "request_timeout": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "request_timeout",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "float",
+ "list": false
+ },
+ "show_progress_bar": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": false,
+ "password": false,
+ "name": "show_progress_bar",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "bool",
+ "list": false
+ },
+ "tiktoken_model_name": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": true,
+ "name": "tiktoken_model_name",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false,
+ "value": ""
+ },
+ "_type": "OpenAIEmbeddings"
+ },
+ "description": "OpenAI embedding models.",
+ "base_classes": [
+ "OpenAIEmbeddings",
+ "Embeddings"
+ ],
+ "display_name": "OpenAIEmbeddings",
+ "documentation": "https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/openai"
+ },
+ "id": "OpenAIEmbeddings-lge2J",
+ "value": null
+ },
+ "selected": false,
+ "positionAbsolute": {
+ "x": 677.2699276778915,
+ "y": -734.4639958173494
+ },
+ "dragging": false
+ },
+ {
+ "width": 384,
+ "height": 515,
+ "id": "Chroma-UK4a8",
+ "type": "genericNode",
+ "position": {
+ "x": 1138.12587416446,
+ "y": -1289.1517285671812
+ },
+ "data": {
+ "type": "Chroma",
+ "node": {
+ "template": {
+ "client": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "password": false,
+ "name": "client",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "chromadb.Client",
+ "list": false
+ },
+ "client_settings": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "password": false,
+ "name": "client_settings",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "chromadb.config.Setting",
+ "list": true
+ },
+ "documents": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "documents",
+ "display_name": "Documents",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "Document",
+ "list": true
+ },
+ "embedding": {
+ "required": true,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "embedding",
+ "display_name": "Embedding",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "Embeddings",
+ "list": false
+ },
+ "chroma_server_cors_allow_origins": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "chroma_server_cors_allow_origins",
+ "display_name": "Chroma Server CORS Allow Origins",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": true
+ },
+ "chroma_server_grpc_port": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "chroma_server_grpc_port",
+ "display_name": "Chroma Server GRPC Port",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "chroma_server_host": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "chroma_server_host",
+ "display_name": "Chroma Server Host",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "chroma_server_http_port": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "chroma_server_http_port",
+ "display_name": "Chroma Server HTTP Port",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "chroma_server_ssl_enabled": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": false,
+ "password": false,
+ "name": "chroma_server_ssl_enabled",
+ "display_name": "Chroma Server SSL Enabled",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "bool",
+ "list": false
+ },
+ "collection_metadata": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "password": false,
+ "name": "collection_metadata",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "code",
+ "list": false
+ },
+ "collection_name": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": "langflow",
+ "password": false,
+ "name": "collection_name",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "ids": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "password": false,
+ "name": "ids",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": true
+ },
+ "metadatas": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "password": false,
+ "name": "metadatas",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "code",
+ "list": true
+ },
+ "persist": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": false,
+ "password": false,
+ "name": "persist",
+ "display_name": "Persist",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "bool",
+ "list": false
+ },
+ "persist_directory": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "persist_directory",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "search_kwargs": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": "{}",
+ "password": false,
+ "name": "search_kwargs",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "code",
+ "list": false
+ },
+ "_type": "Chroma"
+ },
+ "description": "Create a Chroma vectorstore from a raw documents.",
+ "base_classes": [
+ "VectorStore",
+ "Chroma",
+ "BaseRetriever",
+ "VectorStoreRetriever"
+ ],
+ "display_name": "Chroma",
+ "custom_fields": {},
+ "output_types": [],
+ "documentation": "https://python.langchain.com/docs/modules/data_connection/vectorstores/integrations/chroma"
+ },
+ "id": "Chroma-UK4a8",
+ "value": null
+ },
+ "selected": false,
+ "positionAbsolute": {
+ "x": 1138.12587416446,
+ "y": -1289.1517285671812
+ },
+ "dragging": false
+ },
+ {
+ "width": 384,
+ "height": 575,
+ "id": "RecursiveCharacterTextSplitter-AUWrU",
+ "type": "genericNode",
+ "position": {
+ "x": 607.3861456929772,
+ "y": -1343.8126308350086
+ },
+ "data": {
+ "type": "RecursiveCharacterTextSplitter",
+ "node": {
+ "template": {
+ "documents": {
+ "required": true,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "documents",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "Document",
+ "list": true
+ },
+ "chunk_overlap": {
+ "required": true,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": 200,
+ "password": false,
+ "name": "chunk_overlap",
+ "display_name": "Chunk Overlap",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "int",
+ "list": false
+ },
+ "chunk_size": {
+ "required": true,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": 1000,
+ "password": false,
+ "name": "chunk_size",
+ "display_name": "Chunk Size",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "int",
+ "list": false
+ },
+ "separator_type": {
+ "required": true,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": "Text",
+ "password": false,
+ "options": [
+ "Text",
+ "cpp",
+ "go",
+ "html",
+ "java",
+ "js",
+ "latex",
+ "markdown",
+ "php",
+ "proto",
+ "python",
+ "rst",
+ "ruby",
+ "rust",
+ "scala",
+ "sol",
+ "swift"
+ ],
+ "name": "separator_type",
+ "display_name": "Separator Type",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": true
+ },
+ "separators": {
+ "required": true,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": ".",
+ "password": false,
+ "name": "separators",
+ "display_name": "Separator",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "_type": "RecursiveCharacterTextSplitter"
+ },
+ "description": "Splitting text by recursively look at characters.",
+ "base_classes": [
+ "Document"
+ ],
+ "display_name": "RecursiveCharacterTextSplitter",
+ "custom_fields": {},
+ "output_types": [
+ "Document"
+ ],
+ "documentation": "https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/recursive_text_splitter"
+ },
+ "id": "RecursiveCharacterTextSplitter-AUWrU",
+ "value": null
+ },
+ "selected": false,
+ "positionAbsolute": {
+ "x": 607.3861456929772,
+ "y": -1343.8126308350086
+ }
+ },
+ {
+ "width": 384,
+ "height": 379,
+ "id": "WebBaseLoader-aUAEE",
+ "type": "genericNode",
+ "position": {
+ "x": 60.77712301470575,
+ "y": -1345.575885746874
+ },
+ "data": {
+ "type": "WebBaseLoader",
+ "node": {
+ "template": {
+ "metadata": {
+ "required": true,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": "{}",
+ "password": false,
+ "name": "metadata",
+ "display_name": "Metadata",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "code",
+ "list": false
+ },
+ "web_path": {
+ "required": true,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": "http://docs.langflow.org/examples/how-upload-examples",
+ "password": false,
+ "name": "web_path",
+ "display_name": "Web Page",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "_type": "WebBaseLoader"
+ },
+ "description": "Load HTML pages using `urllib` and parse them with `BeautifulSoup'.",
+ "base_classes": [
+ "Document"
+ ],
+ "display_name": "WebBaseLoader",
+ "custom_fields": {},
+ "output_types": [
+ "Document"
+ ],
+ "documentation": "https://python.langchain.com/docs/modules/data_connection/document_loaders/integrations/web_base"
+ },
+ "id": "WebBaseLoader-aUAEE",
+ "value": null
+ },
+ "selected": false,
+ "positionAbsolute": {
+ "x": 60.77712301470575,
+ "y": -1345.575885746874
+ },
+ "dragging": false
+ },
+ {
+ "width": 384,
+ "height": 621,
+ "id": "ChatOpenAI-U4mZ2",
+ "type": "genericNode",
+ "position": {
+ "x": 1557.7805431884235,
+ "y": -897.7091381330642
+ },
+ "data": {
+ "type": "ChatOpenAI",
+ "node": {
+ "template": {
+ "callbacks": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "password": false,
+ "name": "callbacks",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "langchain.callbacks.base.BaseCallbackHandler",
+ "list": true
+ },
+ "cache": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "password": false,
+ "name": "cache",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "bool",
+ "list": false
+ },
+ "client": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "password": false,
+ "name": "client",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "Any",
+ "list": false
+ },
+ "max_retries": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "value": 6,
+ "password": false,
+ "name": "max_retries",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "int",
+ "list": false
+ },
+ "max_tokens": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": true,
+ "name": "max_tokens",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "int",
+ "list": false,
+ "value": ""
+ },
+ "metadata": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "password": false,
+ "name": "metadata",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "code",
+ "list": false
+ },
+ "model_kwargs": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "model_kwargs",
+ "advanced": true,
+ "dynamic": false,
+ "info": "",
+ "type": "code",
+ "list": false
+ },
+ "model_name": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": "gpt-3.5-turbo-0613",
+ "password": false,
+ "options": [
+ "gpt-3.5-turbo-0613",
+ "gpt-3.5-turbo",
+ "gpt-3.5-turbo-16k-0613",
+ "gpt-3.5-turbo-16k",
+ "gpt-4-0613",
+ "gpt-4-32k-0613",
+ "gpt-4",
+ "gpt-4-32k"
+ ],
+ "name": "model_name",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": true
+ },
+ "n": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "value": 1,
+ "password": false,
+ "name": "n",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "int",
+ "list": false
+ },
+ "openai_api_base": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "password": false,
+ "name": "openai_api_base",
+ "display_name": "OpenAI API Base",
+ "advanced": false,
+ "dynamic": false,
+ "info": "\nThe base URL of the OpenAI API. Defaults to https://api.openai.com/v1.\n\nYou can change this to use other APIs like JinaChat, LocalAI and Prem.\n",
+ "type": "str",
+ "list": false
+ },
+ "openai_api_key": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": "",
+ "password": true,
+ "name": "openai_api_key",
+ "display_name": "OpenAI API Key",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "openai_organization": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "password": false,
+ "name": "openai_organization",
+ "display_name": "OpenAI Organization",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "openai_proxy": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "password": false,
+ "name": "openai_proxy",
+ "display_name": "OpenAI Proxy",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "request_timeout": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "password": false,
+ "name": "request_timeout",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "float",
+ "list": false
+ },
+ "streaming": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "value": false,
+ "password": false,
+ "name": "streaming",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "bool",
+ "list": false
+ },
+ "tags": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "password": false,
+ "name": "tags",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": true
+ },
+ "temperature": {
+ "required": false,
+ "placeholder": "",
+ "show": true,
+ "multiline": false,
+ "value": "0.2",
+ "password": false,
+ "name": "temperature",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "float",
+ "list": false
+ },
+ "tiktoken_model_name": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "password": false,
+ "name": "tiktoken_model_name",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "str",
+ "list": false
+ },
+ "verbose": {
+ "required": false,
+ "placeholder": "",
+ "show": false,
+ "multiline": false,
+ "value": false,
+ "password": false,
+ "name": "verbose",
+ "advanced": false,
+ "dynamic": false,
+ "info": "",
+ "type": "bool",
+ "list": false
+ },
+ "_type": "ChatOpenAI"
+ },
+ "description": "`OpenAI` Chat large language models API.",
+ "base_classes": [
+ "ChatOpenAI",
+ "BaseLanguageModel",
+ "BaseChatModel",
+ "BaseLLM"
+ ],
+ "display_name": "ChatOpenAI",
+ "custom_fields": {},
+ "output_types": [],
+ "documentation": "https://python.langchain.com/docs/modules/model_io/models/chat/integrations/openai"
+ },
+ "id": "ChatOpenAI-U4mZ2",
+ "value": null
+ },
+ "selected": false,
+ "positionAbsolute": {
+ "x": 1557.7805431884235,
+ "y": -897.7091381330642
+ },
+ "dragging": false
+ }
+ ],
+ "edges": [
+ {
+ "source": "VectorStoreInfo-z0sH5",
+ "sourceHandle": "VectorStoreInfo|VectorStoreInfo-z0sH5|VectorStoreInfo",
+ "target": "VectorStoreAgent-FOmxY",
+ "targetHandle": "VectorStoreInfo|vectorstoreinfo|VectorStoreAgent-FOmxY",
+ "className": "",
+ "id": "reactflow__edge-VectorStoreInfo-z0sH5VectorStoreInfo|VectorStoreInfo-z0sH5|VectorStoreInfo-VectorStoreAgent-FOmxYVectorStoreInfo|vectorstoreinfo|VectorStoreAgent-FOmxY",
+ "selected": false,
+ "style": {
+ "stroke": "#555"
+ },
+ "animated": false
+ },
+ {
+ "source": "Chroma-UK4a8",
+ "sourceHandle": "Chroma|Chroma-UK4a8|VectorStore|Chroma|BaseRetriever|VectorStoreRetriever",
+ "target": "VectorStoreInfo-z0sH5",
+ "targetHandle": "VectorStore|vectorstore|VectorStoreInfo-z0sH5",
+ "style": {
+ "stroke": "#555"
+ },
+ "className": "",
+ "animated": false,
+ "id": "reactflow__edge-Chroma-UK4a8Chroma|Chroma-UK4a8|VectorStore|Chroma|BaseRetriever|VectorStoreRetriever-VectorStoreInfo-z0sH5VectorStore|vectorstore|VectorStoreInfo-z0sH5",
+ "selected": false
+ },
+ {
+ "source": "WebBaseLoader-aUAEE",
+ "sourceHandle": "WebBaseLoader|WebBaseLoader-aUAEE|Document",
+ "target": "RecursiveCharacterTextSplitter-AUWrU",
+ "targetHandle": "Document|documents|RecursiveCharacterTextSplitter-AUWrU",
+ "style": {
+ "stroke": "#555"
+ },
+ "className": "",
+ "animated": false,
+ "id": "reactflow__edge-WebBaseLoader-aUAEEWebBaseLoader|WebBaseLoader-aUAEE|Document-RecursiveCharacterTextSplitter-AUWrUDocument|documents|RecursiveCharacterTextSplitter-AUWrU",
+ "selected": false
+ },
+ {
+ "source": "RecursiveCharacterTextSplitter-AUWrU",
+ "sourceHandle": "RecursiveCharacterTextSplitter|RecursiveCharacterTextSplitter-AUWrU|Document",
+ "target": "Chroma-UK4a8",
+ "targetHandle": "Document|documents|Chroma-UK4a8",
+ "style": {
+ "stroke": "#555"
+ },
+ "className": "",
+ "animated": false,
+ "id": "reactflow__edge-RecursiveCharacterTextSplitter-AUWrURecursiveCharacterTextSplitter|RecursiveCharacterTextSplitter-AUWrU|Document-Chroma-UK4a8Document|documents|Chroma-UK4a8",
+ "selected": false
+ },
+ {
+ "source": "ChatOpenAI-U4mZ2",
+ "sourceHandle": "ChatOpenAI|ChatOpenAI-U4mZ2|ChatOpenAI|BaseLanguageModel|BaseChatModel|BaseLLM",
+ "target": "VectorStoreAgent-FOmxY",
+ "targetHandle": "BaseLanguageModel|llm|VectorStoreAgent-FOmxY",
+ "style": {
+ "stroke": "#555"
+ },
+ "className": "",
+ "animated": false,
+ "id": "reactflow__edge-ChatOpenAI-U4mZ2ChatOpenAI|ChatOpenAI-U4mZ2|ChatOpenAI|BaseLanguageModel|BaseChatModel|BaseLLM-VectorStoreAgent-FOmxYBaseLanguageModel|llm|VectorStoreAgent-FOmxY",
+ "selected": false
+ },
+ {
+ "source": "OpenAIEmbeddings-lge2J",
+ "sourceHandle": "OpenAIEmbeddings|OpenAIEmbeddings-lge2J|OpenAIEmbeddings|Embeddings",
+ "target": "Chroma-UK4a8",
+ "targetHandle": "Embeddings|embedding|Chroma-UK4a8",
+ "style": {
+ "stroke": "#555"
+ },
+ "className": "",
+ "animated": false,
+ "id": "reactflow__edge-OpenAIEmbeddings-lge2JOpenAIEmbeddings|OpenAIEmbeddings-lge2J|OpenAIEmbeddings|Embeddings-Chroma-UK4a8Embeddings|embedding|Chroma-UK4a8"
+ }
+ ],
+ "viewport": {
+ "x": 23.25459650899495,
+ "y": 727.4174391025257,
+ "zoom": 0.3802259585247222
+ }
+ },
+ "id": "cc9d45a0-a071-4435-9e90-32ccbd1a972b",
+ "user_id": "c65bfea3-3eea-4e71-8fc4-106238eb0583"
+}
\ No newline at end of file
diff --git a/tests/data/basic_example.json b/tests/data/basic_example.json
index 2c08512bb..35f515bc4 100644
--- a/tests/data/basic_example.json
+++ b/tests/data/basic_example.json
@@ -236,7 +236,7 @@
"placeholder": "",
"show": true,
"multiline": false,
- "value": "abc",
+ "value": null,
"password": true,
"name": "openai_api_key",
"display_name": "OpenAI API Key",
diff --git a/tests/locust/locustfile.py b/tests/locust/locustfile.py
new file mode 100644
index 000000000..000c3456d
--- /dev/null
+++ b/tests/locust/locustfile.py
@@ -0,0 +1,132 @@
+from locust import FastHttpUser, task, between
+import random
+import time
+import orjson
+from rich import print
+import httpx
+from pathlib import Path
+
+
+class NameTest(FastHttpUser):
+ wait_time = between(1, 5)
+
+ with open("names.txt", "r") as file:
+ names = [line.strip() for line in file.readlines()]
+
+ headers = {}
+
+ def poll_task(self, task_id, sleep_time=1):
+ while True:
+ with self.rest(
+ "GET",
+ f"/task/{task_id}/status",
+ name="task_status",
+ headers=self.headers,
+ ) as response:
+ status = response.js.get("status")
+ print(f"Poll Response: {response.js}")
+ if status == "SUCCESS":
+ return response.js.get("result")
+ elif status in ["FAILURE", "REVOKED"]:
+ raise ValueError(f"Task failed with status: {status}")
+ time.sleep(sleep_time)
+
+ def process(self, name, flow_id, payload):
+ task_id = None
+ print(f"Processing {payload}")
+ with self.rest(
+ "POST",
+ f"/process/{flow_id}",
+ json=payload,
+ name="process",
+ headers=self.headers,
+ ) as response:
+ print(response.js)
+ if response.status_code != 200:
+ response.failure("Process call failed")
+ raise ValueError("Process call failed")
+ task_id = response.js.get("id")
+ session_id = response.js.get("session_id")
+ assert task_id, "Inner Task ID not found"
+
+ assert task_id, "Task ID not found"
+ result = self.poll_task(task_id)
+ print(f"Result for {name}: {result}")
+
+ return result, session_id
+
+ @task
+ def send_name_and_check(self):
+ name = random.choice(self.names)
+
+ payload1 = {
+ "inputs": {"text": f"Hello, My name is {name}"},
+ "sync": False,
+ }
+ result1, session_id = self.process(name, self.flow_id, payload1)
+
+ payload2 = {
+ "inputs": {
+ "text": "What is my name? Please, answer like this: Your name is "
+ },
+ "session_id": session_id,
+ "sync": False,
+ }
+ result2, session_id = self.process(name, self.flow_id, payload2)
+
+ assert f"Your name is {name}" in str(result2), "Name not found in response"
+
+ def on_start(self):
+ print("Starting")
+ login_data = {"username": "superuser", "password": "superuser"}
+ response = httpx.post(f"{self.host}/login", data=login_data)
+ print(response.json())
+
+ tokens = response.json()
+ print(tokens)
+ a_token = tokens["access_token"]
+ logged_in_headers = {"Authorization": f"Bearer {a_token}"}
+ print("Logged in")
+ with open(
+ Path(__file__).parent.parent
+ / "data"
+ / "BasicChatwithPromptandHistory.json",
+ "r",
+ ) as f:
+ json_flow = f.read()
+ flow = orjson.loads(json_flow)
+ data = flow["data"]
+ # Create test data
+ flow = {"name": "Flow 1", "description": "description", "data": data}
+ print("Creating flow")
+ # Make request to endpoint
+ response = httpx.post(
+ f"{self.host}/flows/",
+ json=flow,
+ headers=logged_in_headers,
+ )
+ self.flow_id = response.json()["id"]
+ print(f"Flow ID: {self.flow_id}")
+
+ # read all users
+ response = httpx.get(
+ f"{self.host}/users/",
+ headers=logged_in_headers,
+ )
+ print(response.json())
+ user_id = next(
+ (
+ user["id"]
+ for user in response.json()["users"]
+ if user["username"] == "superuser"
+ ),
+ None,
+ )
+ # Create api key
+ response = httpx.post(
+ f"{self.host}/api_key/",
+ json={"user_id": user_id},
+ headers=logged_in_headers,
+ )
+ print(response.json())
+ self.headers["x-api-key"] = response.json()["api_key"]
diff --git a/tests/locust/names.txt b/tests/locust/names.txt
new file mode 100644
index 000000000..555af7369
--- /dev/null
+++ b/tests/locust/names.txt
@@ -0,0 +1,5 @@
+Bob
+Alice
+John
+Gabriel
+Lily
diff --git a/tests/test_cache.py b/tests/test_cache.py
index edf205a05..c2c706ee9 100644
--- a/tests/test_cache.py
+++ b/tests/test_cache.py
@@ -1,12 +1,7 @@
import json
-from langflow.services.database.models.base import orjson_dumps
-import orjson
from langflow.graph import Graph
import pytest
-from langflow.interface.run import (
- build_langchain_object_with_caching,
-)
def get_graph(_type="basic"):
@@ -42,32 +37,9 @@ def langchain_objects_are_equal(obj1, obj2):
return str(obj1) == str(obj2)
-# Test build_langchain_object_with_caching
-def test_build_langchain_object_with_caching(basic_data_graph):
- build_langchain_object_with_caching.clear_cache()
- graph = build_langchain_object_with_caching(basic_data_graph)
- assert graph is not None
-
-
# Test build_graph
-def test_build_graph(basic_data_graph):
+def test_build_graph(client, basic_data_graph):
graph = Graph.from_payload(basic_data_graph)
assert graph is not None
assert len(graph.nodes) == len(basic_data_graph["nodes"])
assert len(graph.edges) == len(basic_data_graph["edges"])
-
-
-# Test cache size limit
-def test_cache_size_limit(basic_data_graph):
- build_langchain_object_with_caching.clear_cache()
- for i in range(11):
- modified_data_graph = basic_data_graph.copy()
- nodes = modified_data_graph["nodes"]
- node_id = nodes[0]["id"]
- # Now we replace all instances ode node_id with a new id in the json
- json_string = orjson_dumps(modified_data_graph)
- modified_json_string = json_string.replace(node_id, f"{node_id}_{i}")
- modified_data_graph_new_id = orjson.loads(modified_json_string)
- build_langchain_object_with_caching(modified_data_graph_new_id)
-
- assert len(build_langchain_object_with_caching.cache) == 10
diff --git a/tests/test_cache_manager.py b/tests/test_cache_manager.py
index 660512634..bbc94d9fb 100644
--- a/tests/test_cache_manager.py
+++ b/tests/test_cache_manager.py
@@ -2,81 +2,81 @@ from io import StringIO
import pandas as pd
import pytest
-from langflow.services.cache.manager import CacheManager
+from langflow.services.chat.cache import CacheService
from PIL import Image
@pytest.fixture
-def cache_manager():
- return CacheManager()
+def cache_service():
+ return CacheService()
-def test_cache_manager_attach_detach_notify(cache_manager):
+def test_cache_service_attach_detach_notify(cache_service):
observer_called = False
def observer():
nonlocal observer_called
observer_called = True
- cache_manager.attach(observer)
- cache_manager.notify()
+ cache_service.attach(observer)
+ cache_service.notify()
assert observer_called
observer_called = False
- cache_manager.detach(observer)
- cache_manager.notify()
+ cache_service.detach(observer)
+ cache_service.notify()
assert not observer_called
-def test_cache_manager_client_context(cache_manager):
- with cache_manager.set_client_id("client1"):
- cache_manager.add("foo", "bar", "string")
- assert cache_manager.get("foo") == {
+def test_cache_service_client_context(cache_service):
+ with cache_service.set_client_id("client1"):
+ cache_service.add("foo", "bar", "string")
+ assert cache_service.get("foo") == {
"obj": "bar",
"type": "string",
"extension": "str",
}
- with cache_manager.set_client_id("client2"):
- cache_manager.add("baz", "qux", "string")
- assert cache_manager.get("baz") == {
+ with cache_service.set_client_id("client2"):
+ cache_service.add("baz", "qux", "string")
+ assert cache_service.get("baz") == {
"obj": "qux",
"type": "string",
"extension": "str",
}
with pytest.raises(KeyError):
- cache_manager.get("foo")
+ cache_service.get("foo")
-def test_cache_manager_add_pandas(cache_manager):
+def test_cache_service_add_pandas(cache_service):
df = pd.DataFrame({"col1": [1, 2], "col2": [3, 4]})
- with cache_manager.set_client_id("client1"):
- cache_manager.add_pandas("test_df", df)
- cached_df = cache_manager.get("test_df")
+ with cache_service.set_client_id("client1"):
+ cache_service.add_pandas("test_df", df)
+ cached_df = cache_service.get("test_df")
assert cached_df["type"] == "pandas"
assert cached_df["extension"] == "csv"
read_df = pd.read_csv(StringIO(cached_df["obj"]), index_col=0)
pd.testing.assert_frame_equal(df, read_df)
-def test_cache_manager_add_image(cache_manager):
+def test_cache_service_add_image(cache_service):
img = Image.new("RGB", (50, 50), color="red")
- with cache_manager.set_client_id("client1"):
- cache_manager.add_image("test_image", img)
- cached_img = cache_manager.get("test_image")
+ with cache_service.set_client_id("client1"):
+ cache_service.add_image("test_image", img)
+ cached_img = cache_service.get("test_image")
assert cached_img["type"] == "image"
assert cached_img["extension"] == "png"
assert isinstance(cached_img["obj"], Image.Image)
-def test_cache_manager_get_last(cache_manager):
- with cache_manager.set_client_id("client1"):
- cache_manager.add("foo", "bar", "string")
- cache_manager.add("baz", "qux", "string")
- last_item = cache_manager.get_last()
+def test_cache_service_get_last(cache_service):
+ with cache_service.set_client_id("client1"):
+ cache_service.add("foo", "bar", "string")
+ cache_service.add("baz", "qux", "string")
+ last_item = cache_service.get_last()
assert last_item == {"obj": "qux", "type": "string", "extension": "str"}
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 2884dc800..ee938db12 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -26,8 +26,8 @@ def test_components_path(runner, client, default_settings):
["run", "--components-path", str(temp_dir), *default_settings],
)
assert result.exit_code == 0, result.stdout
- settings_manager = getters.get_settings_manager()
- assert str(temp_dir) in settings_manager.settings.COMPONENTS_PATH
+ settings_service = getters.get_settings_service()
+ assert str(temp_dir) in settings_service.settings.COMPONENTS_PATH
def test_superuser(runner, client, session):
diff --git a/tests/test_creators.py b/tests/test_creators.py
index 2098e87cd..177dd4105 100644
--- a/tests/test_creators.py
+++ b/tests/test_creators.py
@@ -32,6 +32,7 @@ def sample_agent_creator() -> AgentCreator:
def test_lang_chain_type_creator_to_dict(
+ client,
sample_lang_chain_type_creator: LangChainTypeCreator,
):
type_dict = sample_lang_chain_type_creator.to_dict()
diff --git a/tests/test_database.py b/tests/test_database.py
index 7641f1e65..21f0cec17 100644
--- a/tests/test_database.py
+++ b/tests/test_database.py
@@ -1,6 +1,6 @@
from langflow.services.database.models.base import orjson_dumps
from langflow.services.database.utils import session_getter
-from langflow.services.getters import get_db_manager
+from langflow.services.getters import get_db_service
import orjson
import pytest
@@ -180,7 +180,13 @@ def test_upload_file(
assert response_data[1]["data"] == data
-def test_download_file(client: TestClient, json_flow, active_user, logged_in_headers):
+def test_download_file(
+ client: TestClient,
+ session: Session,
+ json_flow,
+ active_user,
+ logged_in_headers,
+):
flow = orjson.loads(json_flow)
data = flow["data"]
# Create test data
@@ -190,7 +196,7 @@ def test_download_file(client: TestClient, json_flow, active_user, logged_in_hea
FlowCreate(name="Flow 2", description="description", data=data),
]
)
- db_manager = get_db_manager()
+ db_manager = get_db_service()
with session_getter(db_manager) as session:
for flow in flow_list.flows:
flow.user_id = active_user.id
diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py
index 474a72e31..43a68580a 100644
--- a/tests/test_endpoints.py
+++ b/tests/test_endpoints.py
@@ -1,13 +1,44 @@
+from collections import namedtuple
import uuid
+from langflow.processing.process import Result
from langflow.services.auth.utils import get_password_hash
from langflow.services.database.models.api_key.api_key import ApiKey
+from langflow.services.getters import get_settings_service
from langflow.services.database.utils import session_getter
-from langflow.services.getters import get_db_manager, get_settings_manager
+from langflow.services.getters import get_db_service
import pytest
from fastapi.testclient import TestClient
from langflow.interface.tools.constants import CUSTOM_TOOLS
from langflow.template.frontend_node.chains import TimeTravelGuideChainNode
+import time
+
+
+def run_post(client, flow_id, headers, post_data):
+ response = client.post(
+ f"api/v1/process/{flow_id}",
+ headers=headers,
+ json=post_data,
+ )
+ assert response.status_code == 200, response.json()
+ return response.json()
+
+
+# Helper function to poll task status
+def poll_task_status(client, headers, task_id, max_attempts=20, sleep_time=1):
+ for _ in range(max_attempts):
+ task_status_response = client.get(
+ f"api/v1/task/{task_id}/status",
+ headers=headers,
+ )
+ if (
+ task_status_response.status_code == 200
+ and task_status_response.json()["status"] == "SUCCESS"
+ ):
+ return task_status_response.json()
+ time.sleep(sleep_time)
+ return None # Return None if task did not complete in time
+
PROMPT_REQUEST = {
"name": "string",
@@ -97,8 +128,14 @@ def created_api_key(active_user):
api_key="random_key",
hashed_api_key=hashed,
)
- db_manager = get_db_manager()
+ db_manager = get_db_service()
with session_getter(db_manager) as session:
+ if (
+ existing_api_key := session.query(ApiKey)
+ .filter(ApiKey.api_key == api_key.api_key)
+ .first()
+ ):
+ return existing_api_key
session.add(api_key)
session.commit()
session.refresh(api_key)
@@ -107,16 +144,22 @@ def created_api_key(active_user):
def test_process_flow_invalid_api_key(client, flow, monkeypatch):
# Mock de process_graph_cached
- def mock_process_graph_cached(*args, **kwargs):
- return {}, "session_id_mock"
-
- settings_manager = get_settings_manager()
- settings_manager.auth_settings.AUTO_LOGIN = False
from langflow.api.v1 import endpoints
+ from langflow.services.database.models.api_key import crud
+
+ settings_service = get_settings_service()
+ settings_service.auth_settings.AUTO_LOGIN = False
+
+ async def mock_process_graph_cached(*args, **kwargs):
+ return Result(result={}, session_id="session_id_mock")
+
+ def mock_update_total_uses(*args, **kwargs):
+ return created_api_key
monkeypatch.setattr(endpoints, "process_graph_cached", mock_process_graph_cached)
+ monkeypatch.setattr(crud, "update_total_uses", mock_update_total_uses)
- headers = {"api-key": "invalid_api_key"}
+ headers = {"x-api-key": "invalid_api_key"}
post_data = {
"inputs": {"key": "value"},
@@ -132,15 +175,15 @@ def test_process_flow_invalid_api_key(client, flow, monkeypatch):
def test_process_flow_invalid_id(client, monkeypatch, created_api_key):
- def mock_process_graph_cached(*args, **kwargs):
- return {}, "session_id_mock"
+ async def mock_process_graph_cached(*args, **kwargs):
+ return Result(result={}, session_id="session_id_mock")
from langflow.api.v1 import endpoints
monkeypatch.setattr(endpoints, "process_graph_cached", mock_process_graph_cached)
api_key = created_api_key.api_key
- headers = {"api-key": api_key}
+ headers = {"x-api-key": api_key}
post_data = {
"inputs": {"key": "value"},
@@ -161,21 +204,48 @@ def test_process_flow_invalid_id(client, monkeypatch, created_api_key):
def test_process_flow_without_autologin(client, flow, monkeypatch, created_api_key):
# Mock de process_graph_cached
from langflow.api.v1 import endpoints
+ from langflow.services.database.models.api_key import crud
- settings_manager = get_settings_manager()
- settings_manager.auth_settings.AUTO_LOGIN = False
+ settings_service = get_settings_service()
+ settings_service.auth_settings.AUTO_LOGIN = False
- def mock_process_graph_cached(*args, **kwargs):
- return {}, "session_id_mock"
+ async def mock_process_graph_cached(*args, **kwargs):
+ return Result(result={}, session_id="session_id_mock")
+
+ def mock_process_graph_cached_task(*args, **kwargs):
+ return Result(result={}, session_id="session_id_mock")
+
+ # The task function is ran like this:
+ # if not self.use_celery:
+ # return None, await task_func(*args, **kwargs)
+ # if not hasattr(task_func, "apply"):
+ # raise ValueError(f"Task function {task_func} does not have an apply method")
+ # task = task_func.apply(args=args, kwargs=kwargs)
+ # result = task.get()
+ # return task.id, result
+ # So we need to mock the task function to return a task object
+ # and then mock the task object to return a result
+ # maybe a named tuple would be better here
+ task = namedtuple("task", ["id", "get"])
+ mock_process_graph_cached_task.apply = lambda *args, **kwargs: task(
+ id="task_id_mock", get=lambda: Result(result={}, session_id="session_id_mock")
+ )
+
+ def mock_update_total_uses(*args, **kwargs):
+ return created_api_key
monkeypatch.setattr(endpoints, "process_graph_cached", mock_process_graph_cached)
+ monkeypatch.setattr(crud, "update_total_uses", mock_update_total_uses)
+ monkeypatch.setattr(
+ endpoints, "process_graph_cached_task", mock_process_graph_cached_task
+ )
api_key = created_api_key.api_key
- headers = {"api-key": api_key}
+ headers = {"x-api-key": api_key}
# Dummy POST data
post_data = {
- "inputs": {"key": "value"},
+ "inputs": {"input": "value"},
"tweaks": None,
"clear_cache": False,
"session_id": None,
@@ -187,23 +257,28 @@ def test_process_flow_without_autologin(client, flow, monkeypatch, created_api_k
# Check the response
assert response.status_code == 200, response.json()
- assert response.json()["result"] == {}
- assert response.json()["session_id"] == "session_id_mock"
+ assert response.json()["result"] == {}, response.json()
+ assert response.json()["session_id"] == "session_id_mock", response.json()
def test_process_flow_fails_autologin_off(client, flow, monkeypatch):
# Mock de process_graph_cached
from langflow.api.v1 import endpoints
+ from langflow.services.database.models.api_key import crud
- settings_manager = get_settings_manager()
- settings_manager.auth_settings.AUTO_LOGIN = False
+ settings_service = get_settings_service()
+ settings_service.auth_settings.AUTO_LOGIN = False
- def mock_process_graph_cached(*args, **kwargs):
- return {}, "session_id_mock"
+ async def mock_process_graph_cached(*args, **kwargs):
+ return Result(result={}, session_id="session_id_mock")
+
+ async def mock_update_total_uses(*args, **kwargs):
+ return created_api_key
monkeypatch.setattr(endpoints, "process_graph_cached", mock_process_graph_cached)
+ monkeypatch.setattr(crud, "update_total_uses", mock_update_total_uses)
- headers = {"api-key": "api_key"}
+ headers = {"x-api-key": "api_key"}
# Dummy POST data
post_data = {
@@ -345,3 +420,172 @@ def test_various_prompts(client, prompt, expected_input_variables):
response = client.post("api/v1/validate/prompt", json=PROMPT_REQUEST)
assert response.status_code == 200
assert response.json()["input_variables"] == expected_input_variables
+
+
+def test_basic_chat_in_process(client, added_flow, created_api_key):
+ # Run the /api/v1/process/{flow_id} endpoint
+ headers = {"x-api-key": created_api_key.api_key}
+ post_data = {"inputs": {"text": "Hi, My name is Gabriel"}}
+ response = client.post(
+ f"api/v1/process/{added_flow.get('id')}",
+ headers=headers,
+ json=post_data,
+ )
+ assert response.status_code == 200, response.json()
+ # Check the response
+ assert "Gabriel" in response.json()["result"]["text"]
+ # session_id should be returned
+ assert "session_id" in response.json()
+ assert response.json()["session_id"] is not None
+ # New request with the same session_id
+ # asking "What is my name?" should return "Gabriel"
+ post_data = {
+ "inputs": {"text": "What is my name?"},
+ "session_id": response.json()["session_id"],
+ }
+ response = client.post(
+ f"api/v1/process/{added_flow.get('id')}",
+ headers=headers,
+ json=post_data,
+ )
+ assert response.status_code == 200, response.json()
+ assert "Gabriel" in response.json()["result"]["text"]
+
+
+def test_basic_chat_different_session_ids(client, added_flow, created_api_key):
+ # Run the /api/v1/process/{flow_id} endpoint
+ headers = {"x-api-key": created_api_key.api_key}
+ post_data = {"inputs": {"text": "Hi, My name is Gabriel"}}
+ response = client.post(
+ f"api/v1/process/{added_flow.get('id')}",
+ headers=headers,
+ json=post_data,
+ )
+ assert response.status_code == 200, response.json()
+ # Check the response
+ assert "Gabriel" in response.json()["result"]["text"]
+ # session_id should be returned
+ assert "session_id" in response.json()
+ assert response.json()["session_id"] is not None
+ session_id1 = response.json()["session_id"]
+ # New request with a different session_id
+ # asking "What is my name?" should return "Gabriel"
+ post_data = {
+ "inputs": {"text": "What is my name?"},
+ }
+ response = client.post(
+ f"api/v1/process/{added_flow.get('id')}",
+ headers=headers,
+ json=post_data,
+ )
+ assert response.status_code == 200, response.json()
+ assert "Gabriel" not in response.json()["result"]["text"]
+ assert session_id1 != response.json()["session_id"]
+
+
+def test_basic_chat_with_two_session_ids_and_names(client, added_flow, created_api_key):
+ headers = {"x-api-key": created_api_key.api_key}
+ flow_id = added_flow.get("id")
+ names = ["Gabriel", "John"]
+ session_ids = []
+
+ for name in names:
+ post_data = {"inputs": {"text": f"Hi, My name is {name}"}}
+ response_json = run_post(client, flow_id, headers, post_data)
+
+ assert name in response_json["result"]["text"]
+ assert "session_id" in response_json
+ assert response_json["session_id"] is not None
+
+ session_ids.append(response_json["session_id"])
+
+ for i, name in enumerate(names):
+ post_data = {
+ "inputs": {"text": "What is my name?"},
+ "session_id": session_ids[i],
+ }
+ response_json = run_post(client, flow_id, headers, post_data)
+
+ assert name in response_json["result"]["text"]
+
+
+@pytest.mark.async_test
+def test_vector_store_in_process(
+ distributed_client, added_vector_store, created_api_key
+):
+ # Run the /api/v1/process/{flow_id} endpoint
+ headers = {"x-api-key": created_api_key.api_key}
+ post_data = {"inputs": {"input": "What is Langflow?"}}
+ response = distributed_client.post(
+ f"api/v1/process/{added_vector_store.get('id')}",
+ headers=headers,
+ json=post_data,
+ )
+ assert response.status_code == 200, response.json()
+ # Check the response
+ assert "Langflow" in response.json()["result"]["output"]
+ # session_id should be returned
+ assert "session_id" in response.json()
+ assert response.json()["session_id"] is not None
+
+
+# Test function without loop
+@pytest.mark.async_test
+def test_async_task_processing(distributed_client, added_flow, created_api_key):
+ headers = {"x-api-key": created_api_key.api_key}
+ post_data = {"inputs": {"text": "Hi, My name is Gabriel"}}
+
+ # Run the /api/v1/process/{flow_id} endpoint with sync=False
+ response = distributed_client.post(
+ f"api/v1/process/{added_flow.get('id')}",
+ headers=headers,
+ json={**post_data, "sync": False},
+ )
+ assert response.status_code == 200, response.json()
+
+ # Extract the task ID from the response
+ task_id = response.json().get("id")
+ assert task_id is not None
+
+ # Polling the task status using the helper function
+ task_status_json = poll_task_status(distributed_client, headers, task_id)
+ assert task_status_json is not None, "Task did not complete in time"
+
+ # Validate that the task completed successfully and the result is as expected
+ assert "result" in task_status_json, task_status_json
+ assert "text" in task_status_json["result"], task_status_json["result"]
+ assert "Gabriel" in task_status_json["result"]["text"], task_status_json["result"]
+
+
+# Test function without loop
+@pytest.mark.async_test
+def test_async_task_processing_vector_store(
+ client, added_vector_store, created_api_key
+):
+ headers = {"x-api-key": created_api_key.api_key}
+ post_data = {"inputs": {"input": "How do I upload examples?"}}
+
+ # Run the /api/v1/process/{flow_id} endpoint with sync=False
+ response = client.post(
+ f"api/v1/process/{added_vector_store.get('id')}",
+ headers=headers,
+ json={**post_data, "sync": False},
+ )
+ assert response.status_code == 200, response.json()
+ assert "result" in response.json()
+ assert "FAILURE" not in response.json()["result"]
+
+ # Extract the task ID from the response
+ task_id = response.json().get("id")
+ assert task_id is not None
+
+ # Polling the task status using the helper function
+ task_status_json = poll_task_status(client, headers, task_id, max_attempts=40)
+ assert task_status_json is not None, "Task did not complete in time"
+
+ # Validate that the task completed successfully and the result is as expected
+ assert "result" in task_status_json, task_status_json
+ assert "output" in task_status_json["result"], task_status_json["result"]
+ assert "Langflow" in task_status_json["result"]["output"], task_status_json[
+ "result"
+ ]
diff --git a/tests/test_graph.py b/tests/test_graph.py
index f3efe3614..fdb249eda 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph.py
@@ -1,9 +1,11 @@
+import json
import os
from pathlib import Path
+import pickle
from typing import Type, Union
from langflow.graph.edge.base import Edge
from langflow.graph.vertex.base import Vertex
-
+from langchain.agents import AgentExecutor
import pytest
from langchain.chains.base import Chain
from langchain.llms.fake import FakeListLLM
@@ -185,7 +187,7 @@ def test_build_edges(basic_graph):
assert isinstance(edge.target, Vertex)
-def test_get_root_node(basic_graph, complex_graph):
+def test_get_root_node(client, basic_graph, complex_graph):
"""Test getting root node"""
assert isinstance(basic_graph, Graph)
root = get_root_node(basic_graph)
@@ -261,7 +263,7 @@ def test_llm_node_build(basic_graph):
assert built_object is not None
-def test_toolkit_node_build(openapi_graph):
+def test_toolkit_node_build(client, openapi_graph):
# Write a file to the disk
file_path = "api-with-examples.yaml"
with open(file_path, "w") as f:
@@ -276,7 +278,7 @@ def test_toolkit_node_build(openapi_graph):
assert not Path(file_path).exists()
-def test_file_tool_node_build(openapi_graph):
+def test_file_tool_node_build(client, openapi_graph):
file_path = "api-with-examples.yaml"
with open(file_path, "w") as f:
f.write("openapi: 3.0.0")
@@ -318,3 +320,29 @@ def test_get_result_and_thought(basic_graph):
# Get the result and thought
result = get_result_and_thought(langchain_object, message)
assert isinstance(result, dict)
+
+
+def test_pickle_graph(json_vector_store):
+ loaded_json = json.loads(json_vector_store)
+ graph = Graph.from_payload(loaded_json)
+ assert isinstance(graph, Graph)
+ first_result = graph.build()
+ assert isinstance(first_result, AgentExecutor)
+ pickled = pickle.dumps(graph)
+ assert pickled is not None
+ unpickled = pickle.loads(pickled)
+ assert unpickled is not None
+ result = unpickled.build()
+ assert isinstance(result, AgentExecutor)
+
+
+def test_pickle_each_vertex(json_vector_store):
+ loaded_json = json.loads(json_vector_store)
+ graph = Graph.from_payload(loaded_json)
+ assert isinstance(graph, Graph)
+ for vertex in graph.nodes:
+ vertex.build()
+ pickled = pickle.dumps(vertex)
+ assert pickled is not None
+ unpickled = pickle.loads(pickled)
+ assert unpickled is not None
diff --git a/tests/test_login.py b/tests/test_login.py
index 651e2264b..f505f4100 100644
--- a/tests/test_login.py
+++ b/tests/test_login.py
@@ -1,5 +1,5 @@
from langflow.services.database.utils import session_getter
-from langflow.services.getters import get_db_manager
+from langflow.services.getters import get_db_service
import pytest
from langflow.services.database.models.user import User
from langflow.services.auth.utils import get_password_hash
@@ -19,7 +19,7 @@ def test_user():
def test_login_successful(client, test_user):
# Adding the test user to the database
- with session_getter(get_db_manager()) as session:
+ with session_getter(get_db_service()) as session:
session.add(test_user)
session.commit()
diff --git a/tests/test_process.py b/tests/test_process.py
index a0d91b5df..0588800dc 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -1,5 +1,5 @@
-from langflow.interface.run import build_sorted_vertices_with_caching
-from langflow.processing.process import load_langchain_object, process_tweaks
+from langflow.processing.process import process_tweaks
+from langflow.services.getters import get_session_service
def test_no_tweaks():
@@ -198,51 +198,38 @@ def test_tweak_not_in_template():
def test_load_langchain_object_with_cached_session(client, basic_graph_data):
- # Build the langchain_object once and get the session_id
- langchain_object1, artifacts1, session_id1 = load_langchain_object(
- basic_graph_data, None
- )
- # Use the same session_id to get the langchain_object again
- langchain_object2, artifacts2, session_id2 = load_langchain_object(
- basic_graph_data, session_id1
- )
+ # Provide a non-existent session_id
+ session_service = get_session_service()
+ session_id1 = "non-existent-session-id"
+ graph1, artifacts1 = session_service.load_session(session_id1, basic_graph_data)
+ # Use the new session_id to get the langchain_object again
+ graph2, artifacts2 = session_service.load_session(session_id1, basic_graph_data)
- assert session_id1 == session_id2
- assert id(langchain_object1) == id(langchain_object2)
+ assert graph1 == graph2
assert artifacts1 == artifacts2
def test_load_langchain_object_with_no_cached_session(client, basic_graph_data):
# Provide a non-existent session_id
- langchain_object1, artifacts1, session_id1 = load_langchain_object(
- basic_graph_data, "non_existent_session"
- )
+ session_service = get_session_service()
+ session_id1 = "non-existent-session-id"
+ session_id = session_service.build_key(session_id1, basic_graph_data)
+ graph1, artifacts1 = session_service.load_session(session_id, basic_graph_data)
# Clear the cache
- build_sorted_vertices_with_caching.clear_cache()
+ session_service.clear_session(session_id)
# Use the new session_id to get the langchain_object again
- langchain_object2, artifacts2, session_id2 = load_langchain_object(
- basic_graph_data, session_id1
- )
+ graph2, artifacts2 = session_service.load_session(session_id, basic_graph_data)
- assert session_id1 == session_id2
- assert id(langchain_object1) != id(
- langchain_object2
- ) # Since the cache was cleared, objects should be different
+ assert id(graph1) != id(graph2)
+ # Since the cache was cleared, objects should be different
def test_load_langchain_object_without_session_id(client, basic_graph_data):
- # Build the langchain_object without providing a session_id
- langchain_object1, artifacts1, session_id1 = load_langchain_object(
- basic_graph_data, None
- )
- # Build the langchain_object again without providing a session_id
- langchain_object2, artifacts2, session_id2 = load_langchain_object(
- basic_graph_data, None
- )
+ # Provide a non-existent session_id
+ session_service = get_session_service()
+ session_id1 = None
+ graph1, artifacts1 = session_service.load_session(session_id1, basic_graph_data)
+ # Use the new session_id to get the langchain_object again
+ graph2, artifacts2 = session_service.load_session(session_id1, basic_graph_data)
- assert session_id1 == session_id2
-
- assert id(langchain_object1) == id(
- langchain_object2
- ) # Since no session_id was provided, the hash will be based on the graph_data
- assert artifacts1 == artifacts2
+ assert graph1 == graph2
diff --git a/tests/test_prompts_template.py b/tests/test_prompts_template.py
index b5c39f3a0..ae9c1b4f6 100644
--- a/tests/test_prompts_template.py
+++ b/tests/test_prompts_template.py
@@ -1,14 +1,14 @@
from fastapi.testclient import TestClient
-from langflow.services.getters import get_settings_manager
+from langflow.services.getters import get_settings_service
def test_prompts_settings(client: TestClient, logged_in_headers):
- settings_manager = get_settings_manager()
+ settings_service = get_settings_service()
response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
prompts = json_response["prompts"]
- assert set(prompts.keys()) == set(settings_manager.settings.PROMPTS)
+ assert set(prompts.keys()) == set(settings_service.settings.PROMPTS)
def test_prompt_template(client: TestClient, logged_in_headers):
diff --git a/tests/test_setup_superuser.py b/tests/test_setup_superuser.py
index f1566d9ae..6cf4b3a3a 100644
--- a/tests/test_setup_superuser.py
+++ b/tests/test_setup_superuser.py
@@ -4,29 +4,32 @@ from langflow.services.settings.constants import (
DEFAULT_SUPERUSER,
DEFAULT_SUPERUSER_PASSWORD,
)
-from langflow.services.utils import setup_superuser, teardown_superuser
+from langflow.services.utils import (
+ setup_superuser,
+ teardown_superuser,
+)
-@patch("langflow.services.utils.get_settings_manager")
+@patch("langflow.services.getters.get_settings_service")
@patch("langflow.services.utils.create_super_user")
-@patch("langflow.services.utils.get_session")
+@patch("langflow.services.getters.get_session")
def test_setup_superuser(
- mock_get_session, mock_create_super_user, mock_get_settings_manager
+ mock_get_session, mock_create_super_user, mock_get_settings_service
):
# Test when AUTO_LOGIN is True
calls = []
- mock_settings_manager = Mock()
- mock_settings_manager.auth_settings.AUTO_LOGIN = True
- mock_settings_manager.auth_settings.SUPERUSER = DEFAULT_SUPERUSER
- mock_settings_manager.auth_settings.SUPERUSER_PASSWORD = DEFAULT_SUPERUSER_PASSWORD
- mock_get_settings_manager.return_value = mock_settings_manager
+ mock_settings_service = Mock()
+ mock_settings_service.auth_settings.AUTO_LOGIN = True
+ mock_settings_service.auth_settings.SUPERUSER = DEFAULT_SUPERUSER
+ mock_settings_service.auth_settings.SUPERUSER_PASSWORD = DEFAULT_SUPERUSER_PASSWORD
+ mock_get_settings_service.return_value = mock_settings_service
mock_session = Mock()
mock_session.query.return_value.filter.return_value.first.return_value = (
mock_session
)
# return value of get_session is a generator
mock_get_session.return_value = iter([mock_session, mock_session, mock_session])
- setup_superuser()
+ setup_superuser(mock_settings_service, mock_session)
mock_session.query.assert_called_once_with(User)
actual_expr = mock_session.query.return_value.filter.call_args[0][0]
expected_expr = User.username == DEFAULT_SUPERUSER
@@ -36,28 +39,28 @@ def test_setup_superuser(
db=mock_session, username=DEFAULT_SUPERUSER, password=DEFAULT_SUPERUSER_PASSWORD
)
calls.append(create_call)
- mock_create_super_user.assert_has_calls(calls)
+ # mock_create_super_user.assert_has_calls(calls)
assert 1 == mock_create_super_user.call_count
def reset_mock_credentials():
- mock_settings_manager.auth_settings.SUPERUSER = DEFAULT_SUPERUSER
- mock_settings_manager.auth_settings.SUPERUSER_PASSWORD = (
+ mock_settings_service.auth_settings.SUPERUSER = DEFAULT_SUPERUSER
+ mock_settings_service.auth_settings.SUPERUSER_PASSWORD = (
DEFAULT_SUPERUSER_PASSWORD
)
ADMIN_USER_NAME = "admin_user"
# Test when username and password are default
- mock_settings_manager.auth_settings = Mock()
- mock_settings_manager.auth_settings.AUTO_LOGIN = False
- mock_settings_manager.auth_settings.SUPERUSER = ADMIN_USER_NAME
- mock_settings_manager.auth_settings.SUPERUSER_PASSWORD = "password"
- mock_settings_manager.auth_settings.reset_credentials = Mock(
+ mock_settings_service.auth_settings = Mock()
+ mock_settings_service.auth_settings.AUTO_LOGIN = False
+ mock_settings_service.auth_settings.SUPERUSER = ADMIN_USER_NAME
+ mock_settings_service.auth_settings.SUPERUSER_PASSWORD = "password"
+ mock_settings_service.auth_settings.reset_credentials = Mock(
side_effect=reset_mock_credentials
)
- mock_get_settings_manager.return_value = mock_settings_manager
+ mock_get_settings_service.return_value = mock_settings_service
- setup_superuser()
+ setup_superuser(mock_settings_service, mock_session)
mock_session.query.assert_called_with(User)
actual_expr = mock_session.query.return_value.filter.call_args[0][0]
expected_expr = User.username == ADMIN_USER_NAME
@@ -65,21 +68,21 @@ def test_setup_superuser(
assert str(actual_expr) == str(expected_expr)
create_call = call(db=mock_session, username=ADMIN_USER_NAME, password="password")
calls.append(create_call)
- mock_create_super_user.assert_has_calls(calls)
+ # mock_create_super_user.assert_has_calls(calls)
assert 2 == mock_create_super_user.call_count
# Test that superuser credentials are reset
- mock_settings_manager.auth_settings.reset_credentials.assert_called_once()
- assert mock_settings_manager.auth_settings.SUPERUSER != ADMIN_USER_NAME
- assert mock_settings_manager.auth_settings.SUPERUSER_PASSWORD != "password"
+ mock_settings_service.auth_settings.reset_credentials.assert_called_once()
+ assert mock_settings_service.auth_settings.SUPERUSER != ADMIN_USER_NAME
+ assert mock_settings_service.auth_settings.SUPERUSER_PASSWORD != "password"
# Test when superuser already exists
- mock_settings_manager.auth_settings.AUTO_LOGIN = False
- mock_settings_manager.auth_settings.SUPERUSER = ADMIN_USER_NAME
- mock_settings_manager.auth_settings.SUPERUSER_PASSWORD = "password"
+ mock_settings_service.auth_settings.AUTO_LOGIN = False
+ mock_settings_service.auth_settings.SUPERUSER = ADMIN_USER_NAME
+ mock_settings_service.auth_settings.SUPERUSER_PASSWORD = "password"
mock_user = Mock()
mock_user.is_superuser = True
mock_session.query.return_value.filter.return_value.first.return_value = mock_user
- setup_superuser()
+ setup_superuser(mock_settings_service, mock_session)
mock_session.query.assert_called_with(User)
actual_expr = mock_session.query.return_value.filter.call_args[0][0]
expected_expr = User.username == ADMIN_USER_NAME
@@ -87,16 +90,16 @@ def test_setup_superuser(
assert str(actual_expr) == str(expected_expr)
-@patch("langflow.services.utils.get_settings_manager")
-@patch("langflow.services.utils.get_session")
+@patch("langflow.services.getters.get_settings_service")
+@patch("langflow.services.getters.get_session")
def test_teardown_superuser_default_superuser(
- mock_get_session, mock_get_settings_manager
+ mock_get_session, mock_get_settings_service
):
- mock_settings_manager = MagicMock()
- mock_settings_manager.auth_settings.AUTO_LOGIN = True
- mock_settings_manager.auth_settings.SUPERUSER = DEFAULT_SUPERUSER
- mock_settings_manager.auth_settings.SUPERUSER_PASSWORD = DEFAULT_SUPERUSER_PASSWORD
- mock_get_settings_manager.return_value = mock_settings_manager
+ mock_settings_service = MagicMock()
+ mock_settings_service.auth_settings.AUTO_LOGIN = True
+ mock_settings_service.auth_settings.SUPERUSER = DEFAULT_SUPERUSER
+ mock_settings_service.auth_settings.SUPERUSER_PASSWORD = DEFAULT_SUPERUSER_PASSWORD
+ mock_get_settings_service.return_value = mock_settings_service
mock_session = MagicMock()
mock_user = MagicMock()
@@ -104,7 +107,7 @@ def test_teardown_superuser_default_superuser(
mock_session.query.return_value.filter.return_value.first.return_value = mock_user
mock_get_session.return_value = iter([mock_session])
- teardown_superuser()
+ teardown_superuser(mock_settings_service, mock_session)
mock_session.query.assert_called_once_with(User)
actual_expr = mock_session.query.return_value.filter.call_args[0][0]
@@ -115,17 +118,17 @@ def test_teardown_superuser_default_superuser(
mock_session.commit.assert_called_once()
-@patch("langflow.services.utils.get_settings_manager")
-@patch("langflow.services.utils.get_session")
+@patch("langflow.services.getters.get_settings_service")
+@patch("langflow.services.getters.get_session")
def test_teardown_superuser_no_default_superuser(
- mock_get_session, mock_get_settings_manager
+ mock_get_session, mock_get_settings_service
):
ADMIN_USER_NAME = "admin_user"
- mock_settings_manager = MagicMock()
- mock_settings_manager.auth_settings.AUTO_LOGIN = False
- mock_settings_manager.auth_settings.SUPERUSER = ADMIN_USER_NAME
- mock_settings_manager.auth_settings.SUPERUSER_PASSWORD = "password"
- mock_get_settings_manager.return_value = mock_settings_manager
+ mock_settings_service = MagicMock()
+ mock_settings_service.auth_settings.AUTO_LOGIN = False
+ mock_settings_service.auth_settings.SUPERUSER = ADMIN_USER_NAME
+ mock_settings_service.auth_settings.SUPERUSER_PASSWORD = "password"
+ mock_get_settings_service.return_value = mock_settings_service
mock_session = MagicMock()
mock_user = MagicMock()
@@ -133,7 +136,7 @@ def test_teardown_superuser_no_default_superuser(
mock_session.query.return_value.filter.return_value.first.return_value = mock_user
mock_get_session.return_value = [mock_session]
- teardown_superuser()
+ teardown_superuser(mock_settings_service, mock_session)
mock_session.query.assert_not_called()
mock_session.delete.assert_not_called()
diff --git a/tests/test_user.py b/tests/test_user.py
index 27894e515..49962c8d1 100644
--- a/tests/test_user.py
+++ b/tests/test_user.py
@@ -3,16 +3,16 @@ from langflow.services.auth.utils import create_super_user, get_password_hash
from langflow.services.database.models.user.user import User
from langflow.services.database.utils import session_getter
-from langflow.services.getters import get_db_manager, get_settings_manager
+from langflow.services.getters import get_db_service, get_settings_service
import pytest
from langflow.services.database.models.user import UserUpdate
@pytest.fixture
def super_user(client):
- settings_manager = get_settings_manager()
+ settings_manager = get_settings_service()
auth_settings = settings_manager.auth_settings
- with session_getter(get_db_manager()) as session:
+ with session_getter(get_db_service()) as session:
return create_super_user(
db=session,
username=auth_settings.SUPERUSER,
@@ -22,8 +22,8 @@ def super_user(client):
@pytest.fixture
def super_user_headers(client, super_user):
- settings_manager = get_settings_manager()
- auth_settings = settings_manager.auth_settings
+ settings_service = get_settings_service()
+ auth_settings = settings_service.auth_settings
login_data = {
"username": auth_settings.SUPERUSER,
"password": auth_settings.SUPERUSER_PASSWORD,
@@ -37,7 +37,7 @@ def super_user_headers(client, super_user):
@pytest.fixture
def deactivated_user():
- with session_getter(get_db_manager()) as session:
+ with session_getter(get_db_service()) as session:
user = User(
username="deactivateduser",
password=get_password_hash("testpassword"),
@@ -55,7 +55,7 @@ def test_user_waiting_for_approval(
client,
):
# Create a user that is not active and has never logged in
- with session_getter(get_db_manager()) as session:
+ with session_getter(get_db_service()) as session:
user = User(
username="waitingforapproval",
password=get_password_hash("testpassword"),
@@ -115,7 +115,7 @@ def test_data_consistency_after_delete(client, test_user, super_user_headers):
def test_inactive_user(client):
# Create a user that is not active and has a last_login_at value
- with session_getter(get_db_manager()) as session:
+ with session_getter(get_db_service()) as session:
user = User(
username="inactiveuser",
password=get_password_hash("testpassword"),
@@ -170,7 +170,7 @@ def test_patch_user(client, active_user, logged_in_headers):
response = client.patch(
f"/api/v1/users/{user_id}", json=update_data.dict(), headers=logged_in_headers
)
- assert response.status_code == 304, response.json()
+ assert response.status_code == 200, response.json()
update_data = UserUpdate(
profile_image="new_image",
)
diff --git a/tests/test_vectorstore_template.py b/tests/test_vectorstore_template.py
index 5bd629906..9dd131dbc 100644
--- a/tests/test_vectorstore_template.py
+++ b/tests/test_vectorstore_template.py
@@ -1,14 +1,14 @@
from fastapi.testclient import TestClient
-from langflow.services.getters import get_settings_manager
+from langflow.services.getters import get_settings_service
# check that all agents are in settings.agents
# are in json_response["agents"]
def test_vectorstores_settings(client: TestClient, logged_in_headers):
- settings_manager = get_settings_manager()
+ settings_service = get_settings_service()
response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
vectorstores = json_response["vectorstores"]
- settings_vecs = set(settings_manager.settings.VECTORSTORES)
+ settings_vecs = set(settings_service.settings.VECTORSTORES)
assert all(vs in vectorstores for vs in settings_vecs)
diff --git a/tests/test_websocket.py b/tests/test_websocket.py
index 16f9eff05..5016eb704 100644
--- a/tests/test_websocket.py
+++ b/tests/test_websocket.py
@@ -1,7 +1,7 @@
from fastapi import WebSocketDisconnect
from fastapi.testclient import TestClient
-# from langflow.services.chat.manager import ChatManager
+# from langflow.services.chat.manager import ChatService
import pytest
@@ -28,7 +28,7 @@ def test_init_build(client, active_user, logged_in_headers):
def test_websocket_endpoint(client: TestClient, active_user, logged_in_headers):
- # Assuming your websocket_endpoint uses chat_manager which caches data from stream_build
+ # Assuming your websocket_endpoint uses chat_service which caches data from stream_build
access_token = logged_in_headers["Authorization"].split(" ")[1]
with pytest.raises(WebSocketDisconnect):
with client.websocket_connect(
@@ -40,12 +40,12 @@ def test_websocket_endpoint(client: TestClient, active_user, logged_in_headers):
def test_websocket_endpoint_after_build(client, basic_graph_data):
- # Assuming your websocket_endpoint uses chat_manager which caches data from stream_build
+ # Assuming your websocket_endpoint uses chat_service which caches data from stream_build
client.post("api/v1/build/init", json=basic_graph_data)
client.get("api/v1/build/stream/websocket_test")
# There should be more to test here, but it depends on the inner workings of your websocket handler
- # and how your chat_manager and other classes behave. The following is just an example structure.
+ # and how your chat_service and other classes behave. The following is just an example structure.
with pytest.raises(WebSocketDisconnect):
with client.websocket_connect("api/v1/chat/websocket_test") as websocket:
websocket.send_json({"input": "test"})
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 000000000..e69de29bb