Merge branch 'dev' of https://github.com/logspace-ai/langflow into dev
This commit is contained in:
commit
b4ea2478e8
29 changed files with 494 additions and 181 deletions
2
Makefile
2
Makefile
|
|
@ -20,7 +20,7 @@ coverage:
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
@make install_backend
|
@make install_backend
|
||||||
poetry run pytest tests -n auto
|
poetry run pytest tests
|
||||||
|
|
||||||
format:
|
format:
|
||||||
poetry run black .
|
poetry run black .
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
proxy:
|
proxy:
|
||||||
ports:
|
ports:
|
||||||
|
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
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
|
|
||||||
277
deploy/docker-compose.with_tests.yml
Normal file
277
deploy/docker-compose.with_tests.yml
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
@ -27,13 +27,29 @@ curl -X POST \
|
||||||
-d '{"inputs": {"text": ""}, "tweaks": {}, "sync": false}'
|
-d '{"inputs": {"text": ""}, "tweaks": {}, "sync": false}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"output": "..."
|
||||||
|
},
|
||||||
|
"task": {
|
||||||
|
"id": "...",
|
||||||
|
"href": "api/v1/task/<task_id>"
|
||||||
|
},
|
||||||
|
"session_id": "...",
|
||||||
|
"backend": "..." // celery or anyio
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Checking Task Status
|
## Checking Task Status
|
||||||
|
|
||||||
You can check the status of an asynchronous task by making a GET request to the `/task/{task_id}/status` endpoint.
|
You can check the status of an asynchronous task by making a GET request to the `/task/{task_id}` endpoint.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X GET \
|
curl -X GET \
|
||||||
http://localhost:3000/api/v1/task/<task_id>/status \
|
http://localhost:3000/api/v1/task/<task_id> \
|
||||||
-H 'x-api-key: <your_api_key>'
|
-H 'x-api-key: <your_api_key>'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,5 +40,5 @@ This will set up the following containers:
|
||||||
To run the tests for the Async API, you can run the following command:
|
To run the tests for the Async API, you can run the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.test.yml up --exit-code-from tests tests result_backend broker celeryworker db --build
|
docker compose -f docker-compose.with_tests.yml up --exit-code-from tests tests result_backend broker celeryworker db --build
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ from rich.table import Table
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer(no_args_is_help=True)
|
||||||
|
|
||||||
|
|
||||||
def get_number_of_workers(workers=None):
|
def get_number_of_workers(workers=None):
|
||||||
|
|
@ -141,7 +141,7 @@ def run(
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Run the Langflow server.
|
Run the Langflow.
|
||||||
"""
|
"""
|
||||||
# override env variables with .env file
|
# override env variables with .env file
|
||||||
if env_file:
|
if env_file:
|
||||||
|
|
@ -299,7 +299,14 @@ def superuser(
|
||||||
password: str = typer.Option(
|
password: str = typer.Option(
|
||||||
..., prompt=True, hide_input=True, help="Password for the superuser."
|
..., prompt=True, hide_input=True, help="Password for the superuser."
|
||||||
),
|
),
|
||||||
|
log_level: str = typer.Option(
|
||||||
|
"critical", help="Logging level.", envvar="LANGFLOW_LOG_LEVEL"
|
||||||
|
),
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Create a superuser.
|
||||||
|
"""
|
||||||
|
configure(log_level=log_level)
|
||||||
initialize_services()
|
initialize_services()
|
||||||
db_service = get_db_service()
|
db_service = get_db_service()
|
||||||
with session_getter(db_service) as session:
|
with session_getter(db_service) as session:
|
||||||
|
|
@ -321,7 +328,10 @@ def superuser(
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def migration(test: bool = typer.Option(False, help="Run migrations in test mode.")):
|
def migration(test: bool = typer.Option(True, help="Run migrations in test mode.")):
|
||||||
|
"""
|
||||||
|
Run or test migrations.
|
||||||
|
"""
|
||||||
initialize_services()
|
initialize_services()
|
||||||
db_service = get_db_service()
|
db_service = get_db_service()
|
||||||
if not test:
|
if not test:
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from langflow.api.v1.schemas import BuildStatus, BuiltResponse, InitResponse, St
|
||||||
|
|
||||||
from langflow.graph.graph.base import Graph
|
from langflow.graph.graph.base import Graph
|
||||||
from langflow.services.auth.utils import get_current_active_user, get_current_user
|
from langflow.services.auth.utils import get_current_active_user, get_current_user
|
||||||
|
from langflow.services.cache.utils import update_build_status
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from langflow.services.getters import get_chat_service, get_session, get_cache_service
|
from langflow.services.getters import get_chat_service, get_session, get_cache_service
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
|
|
@ -157,7 +158,7 @@ async def stream_build(
|
||||||
graph = Graph.from_payload(graph_data)
|
graph = Graph.from_payload(graph_data)
|
||||||
|
|
||||||
number_of_nodes = len(graph.nodes)
|
number_of_nodes = len(graph.nodes)
|
||||||
cache_service[flow_id]["status"] = BuildStatus.IN_PROGRESS
|
update_build_status(cache_service, flow_id, BuildStatus.IN_PROGRESS)
|
||||||
|
|
||||||
for i, vertex in enumerate(graph.generator_build(), 1):
|
for i, vertex in enumerate(graph.generator_build(), 1):
|
||||||
try:
|
try:
|
||||||
|
|
@ -184,7 +185,7 @@ async def stream_build(
|
||||||
logger.exception(exc)
|
logger.exception(exc)
|
||||||
params = str(exc)
|
params = str(exc)
|
||||||
valid = False
|
valid = False
|
||||||
cache_service[flow_id]["status"] = BuildStatus.FAILURE
|
update_build_status(cache_service, flow_id, BuildStatus.FAILURE)
|
||||||
|
|
||||||
response = {
|
response = {
|
||||||
"valid": valid,
|
"valid": valid,
|
||||||
|
|
@ -211,11 +212,12 @@ async def stream_build(
|
||||||
chat_service.set_cache(flow_id, langchain_object)
|
chat_service.set_cache(flow_id, langchain_object)
|
||||||
# We need to reset the chat history
|
# We need to reset the chat history
|
||||||
chat_service.chat_history.empty_history(flow_id)
|
chat_service.chat_history.empty_history(flow_id)
|
||||||
cache_service[flow_id]["status"] = BuildStatus.SUCCESS
|
update_build_status(cache_service, flow_id, BuildStatus.SUCCESS)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception(exc)
|
logger.exception(exc)
|
||||||
logger.error("Error while building the flow: %s", exc)
|
logger.error("Error while building the flow: %s", exc)
|
||||||
cache_service[flow_id]["status"] = BuildStatus.FAILURE
|
|
||||||
|
update_build_status(cache_service, flow_id, BuildStatus.FAILURE)
|
||||||
yield str(StreamData(event="error", data={"error": str(exc)}))
|
yield str(StreamData(event="error", data={"error": str(exc)}))
|
||||||
finally:
|
finally:
|
||||||
yield str(StreamData(event="message", data=final_response))
|
yield str(StreamData(event="message", data=final_response))
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ from langflow.interface.custom.custom_component import CustomComponent
|
||||||
|
|
||||||
from langflow.api.v1.schemas import (
|
from langflow.api.v1.schemas import (
|
||||||
ProcessResponse,
|
ProcessResponse,
|
||||||
|
TaskResponse,
|
||||||
TaskStatusResponse,
|
TaskStatusResponse,
|
||||||
UploadFileResponse,
|
UploadFileResponse,
|
||||||
CustomComponentCode,
|
CustomComponentCode,
|
||||||
|
|
@ -145,9 +146,15 @@ async def process_flow(
|
||||||
session_id,
|
session_id,
|
||||||
)
|
)
|
||||||
task_result = task.status
|
task_result = task.status
|
||||||
|
|
||||||
|
if task_id:
|
||||||
|
task_response = TaskResponse(id=task_id, href=f"api/v1/task/{task_id}")
|
||||||
|
else:
|
||||||
|
task_response = None
|
||||||
|
|
||||||
return ProcessResponse(
|
return ProcessResponse(
|
||||||
result=task_result,
|
result=task_result,
|
||||||
id=task_id,
|
task=task_response,
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
backend=str(type(task_service.backend)),
|
backend=str(type(task_service.backend)),
|
||||||
)
|
)
|
||||||
|
|
@ -173,7 +180,7 @@ async def process_flow(
|
||||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
@router.get("/task/{task_id}/status", response_model=TaskStatusResponse)
|
@router.get("/task/{task_id}", response_model=TaskStatusResponse)
|
||||||
async def get_task_status(task_id: str):
|
async def get_task_status(task_id: str):
|
||||||
task_service = get_task_service()
|
task_service = get_task_service()
|
||||||
task = task_service.get_task(task_id)
|
task = task_service.get_task(task_id)
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,18 @@ class UpdateTemplateRequest(BaseModel):
|
||||||
template: dict
|
template: dict
|
||||||
|
|
||||||
|
|
||||||
|
class TaskResponse(BaseModel):
|
||||||
|
"""Task response schema."""
|
||||||
|
|
||||||
|
id: Optional[str] = Field(None)
|
||||||
|
href: Optional[str] = Field(None)
|
||||||
|
|
||||||
|
|
||||||
class ProcessResponse(BaseModel):
|
class ProcessResponse(BaseModel):
|
||||||
"""Process response schema."""
|
"""Process response schema."""
|
||||||
|
|
||||||
result: Any
|
result: Any
|
||||||
id: Optional[str] = None
|
task: Optional[TaskResponse] = None
|
||||||
session_id: Optional[str] = None
|
session_id: Optional[str] = None
|
||||||
backend: Optional[str] = None
|
backend: Optional[str] = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -185,9 +185,10 @@ class Vertex:
|
||||||
# Load the type in value.get('suffixes') using
|
# Load the type in value.get('suffixes') using
|
||||||
# what is inside value.get('content')
|
# what is inside value.get('content')
|
||||||
# value.get('value') is the file name
|
# value.get('value') is the file name
|
||||||
file_path = value.get("file_path")
|
if file_path := value.get("file_path"):
|
||||||
|
params[key] = file_path
|
||||||
params[key] = file_path
|
else:
|
||||||
|
raise ValueError(f"File path not found for {self.vertex_type}")
|
||||||
elif value.get("type") in DIRECT_TYPES and params.get(key) is None:
|
elif value.get("type") in DIRECT_TYPES and params.get(key) is None:
|
||||||
if value.get("type") == "code":
|
if value.get("type") == "code":
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,10 @@ class CustomComponent(Component, extra=Extra.allow):
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail={
|
detail={
|
||||||
"error": "Type hint Error",
|
"error": "Type hint Error",
|
||||||
"traceback": "Prompt type is not supported in the build method. Try using PromptTemplate instead.",
|
"traceback": (
|
||||||
|
"Prompt type is not supported in the build method."
|
||||||
|
" Try using PromptTemplate instead."
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return args
|
return args
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,8 @@ def import_chain(chain: str) -> Type[Chain]:
|
||||||
|
|
||||||
if chain in CUSTOM_CHAINS:
|
if chain in CUSTOM_CHAINS:
|
||||||
return CUSTOM_CHAINS[chain]
|
return CUSTOM_CHAINS[chain]
|
||||||
|
if chain == "SQLDatabaseChain":
|
||||||
|
return import_class("langchain_experimental.sql.SQLDatabaseChain")
|
||||||
return import_class(f"langchain.chains.{chain}")
|
return import_class(f"langchain.chains.{chain}")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
16
src/backend/langflow/services/cache/manager.py
vendored
16
src/backend/langflow/services/cache/manager.py
vendored
|
|
@ -74,13 +74,17 @@ class InMemoryCache(BaseCacheService, Service):
|
||||||
):
|
):
|
||||||
# Move the key to the end to make it recently used
|
# Move the key to the end to make it recently used
|
||||||
self._cache.move_to_end(key)
|
self._cache.move_to_end(key)
|
||||||
unpickled = pickle.loads(item["value"])
|
# Check if the value is pickled
|
||||||
return unpickled
|
if isinstance(item["value"], bytes):
|
||||||
|
value = pickle.loads(item["value"])
|
||||||
|
else:
|
||||||
|
value = item["value"]
|
||||||
|
return value
|
||||||
else:
|
else:
|
||||||
self.delete(key)
|
self.delete(key)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set(self, key, value):
|
def set(self, key, value, pickle=False):
|
||||||
"""
|
"""
|
||||||
Add an item to the cache.
|
Add an item to the cache.
|
||||||
|
|
||||||
|
|
@ -98,8 +102,10 @@ class InMemoryCache(BaseCacheService, Service):
|
||||||
# Remove least recently used item
|
# Remove least recently used item
|
||||||
self._cache.popitem(last=False)
|
self._cache.popitem(last=False)
|
||||||
# pickle locally to mimic Redis
|
# pickle locally to mimic Redis
|
||||||
pickled = pickle.dumps(value)
|
if pickle:
|
||||||
self._cache[key] = {"value": pickled, "time": time.time()}
|
value = pickle.dumps(value)
|
||||||
|
|
||||||
|
self._cache[key] = {"value": value, "time": time.time()}
|
||||||
|
|
||||||
def upsert(self, key, value):
|
def upsert(self, key, value):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
9
src/backend/langflow/services/cache/utils.py
vendored
9
src/backend/langflow/services/cache/utils.py
vendored
|
|
@ -9,6 +9,7 @@ from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Dict
|
from typing import TYPE_CHECKING, Any, Dict
|
||||||
from appdirs import user_cache_dir
|
from appdirs import user_cache_dir
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
|
from langflow.api.v1.schemas import BuildStatus
|
||||||
from langflow.services.database.models.base import orjson_dumps
|
from langflow.services.database.models.base import orjson_dumps
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -202,3 +203,11 @@ def save_uploaded_file(file: UploadFile, folder_name):
|
||||||
new_file.write(chunk)
|
new_file.write(chunk)
|
||||||
|
|
||||||
return file_path
|
return file_path
|
||||||
|
|
||||||
|
|
||||||
|
def update_build_status(cache_service, flow_id: str, status: BuildStatus):
|
||||||
|
cached_flow = cache_service[flow_id]
|
||||||
|
if cached_flow is None:
|
||||||
|
raise ValueError(f"Flow {flow_id} not found in cache")
|
||||||
|
cached_flow["status"] = status
|
||||||
|
cache_service[flow_id] = cached_flow
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,7 @@ class DatabaseService(Service):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def run_migrations(self):
|
def run_migrations(self):
|
||||||
logger.info(
|
logger.info(f"Running DB migrations in {self.script_location}")
|
||||||
f"Running DB migrations in {self.script_location} on {self.database_url}"
|
|
||||||
)
|
|
||||||
alembic_cfg = Config()
|
alembic_cfg = Config()
|
||||||
alembic_cfg.set_main_option("script_location", str(self.script_location))
|
alembic_cfg.set_main_option("script_location", str(self.script_location))
|
||||||
alembic_cfg.set_main_option("sqlalchemy.url", self.database_url)
|
alembic_cfg.set_main_option("sqlalchemy.url", self.database_url)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from typing import Any, Callable, Coroutine, Union
|
from typing import Any, Callable, Coroutine, Union
|
||||||
|
from langflow.utils.logger import configure
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from langflow.services.base import Service
|
from langflow.services.base import Service
|
||||||
from langflow.services.task.backends.anyio import AnyIOBackend
|
from langflow.services.task.backends.anyio import AnyIOBackend
|
||||||
|
|
@ -7,18 +8,19 @@ from langflow.services.task.utils import get_celery_worker_status
|
||||||
|
|
||||||
|
|
||||||
def check_celery_availability():
|
def check_celery_availability():
|
||||||
from langflow.worker import celery_app
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from langflow.worker import celery_app
|
||||||
|
|
||||||
status = get_celery_worker_status(celery_app)
|
status = get_celery_worker_status(celery_app)
|
||||||
logger.debug(f"Celery status: {status}")
|
logger.debug(f"Celery status: {status}")
|
||||||
except Exception as e:
|
except Exception as exc:
|
||||||
logger.error(f"An error occurred: {e}")
|
logger.debug(f"Celery not available: {exc}")
|
||||||
status = {"availability": None}
|
status = {"availability": None}
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
configure()
|
||||||
status = check_celery_availability()
|
status = check_celery_availability()
|
||||||
|
|
||||||
USE_CELERY = status.get("availability") is not None
|
USE_CELERY = status.get("availability") is not None
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ class DocumentLoaderFrontNode(FrontendNode):
|
||||||
self.template.add_field(
|
self.template.add_field(
|
||||||
TemplateField(
|
TemplateField(
|
||||||
field_type="dict",
|
field_type="dict",
|
||||||
required=True,
|
required=False,
|
||||||
show=True,
|
show=True,
|
||||||
name="metadata",
|
name="metadata",
|
||||||
value={},
|
value={},
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,42 @@ from typing import Optional
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
|
import os
|
||||||
|
import orjson
|
||||||
|
import appdirs
|
||||||
|
|
||||||
|
|
||||||
def configure(log_level: str = "DEBUG", log_file: Optional[Path] = None):
|
VALID_LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
||||||
log_format = "<green>{time:HH:mm:ss}</green> - <level>{level: <8}</level> - <level>{message}</level>"
|
|
||||||
|
|
||||||
|
def serialize(record):
|
||||||
|
subset = {
|
||||||
|
"timestamp": record["time"].timestamp(),
|
||||||
|
"message": record["message"],
|
||||||
|
"level": record["level"].name,
|
||||||
|
"module": record["module"],
|
||||||
|
}
|
||||||
|
return orjson.dumps(subset)
|
||||||
|
|
||||||
|
|
||||||
|
def patching(record):
|
||||||
|
record["extra"]["serialized"] = serialize(record)
|
||||||
|
|
||||||
|
|
||||||
|
def configure(log_level: Optional[str] = None, log_file: Optional[Path] = None):
|
||||||
|
if os.getenv("LANGFLOW_LOG_LEVEL") in VALID_LOG_LEVELS and log_level is None:
|
||||||
|
log_level = os.getenv("LANGFLOW_LOG_LEVEL")
|
||||||
|
if log_level is None:
|
||||||
|
log_level = "INFO"
|
||||||
|
# Human-readable
|
||||||
|
log_format = (
|
||||||
|
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> - <level>"
|
||||||
|
"{level: <8}</level> - {module} - <level>{message}</level>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# log_format = log_format_dev if log_level.upper() == "DEBUG" else log_format_prod
|
||||||
logger.remove() # Remove default handlers
|
logger.remove() # Remove default handlers
|
||||||
|
logger.patch(patching)
|
||||||
# Configure loguru to use RichHandler
|
# Configure loguru to use RichHandler
|
||||||
logger.configure(
|
logger.configure(
|
||||||
handlers=[
|
handlers=[
|
||||||
|
|
@ -19,17 +49,21 @@ def configure(log_level: str = "DEBUG", log_file: Optional[Path] = None):
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
if log_file:
|
if not log_file:
|
||||||
log_file = Path(log_file)
|
cache_dir = Path(appdirs.user_cache_dir("langflow"))
|
||||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
log_file = cache_dir / "langflow.log"
|
||||||
|
|
||||||
logger.add(
|
log_file = Path(log_file)
|
||||||
sink=str(log_file),
|
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
level=log_level.upper(),
|
|
||||||
format=log_format,
|
|
||||||
rotation="10 MB", # Log rotation based on file size
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"Logger set up with log level: {log_level}")
|
logger.add(
|
||||||
|
sink=str(log_file),
|
||||||
|
level=log_level.upper(),
|
||||||
|
format=log_format,
|
||||||
|
rotation="10 MB", # Log rotation based on file size
|
||||||
|
serialize=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"Logger set up with log level: {log_level}")
|
||||||
if log_file:
|
if log_file:
|
||||||
logger.info(f"Log file: {log_file}")
|
logger.info(f"Log file: {log_file}")
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,6 @@ export default function GenericNode({
|
||||||
setValidationStatus(null);
|
setValidationStatus(null);
|
||||||
}
|
}
|
||||||
}, [sseData, data.id]);
|
}, [sseData, data.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NodeToolbar>
|
<NodeToolbar>
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,10 @@ export default function BuildTrigger({
|
||||||
if (isBuilding) {
|
if (isBuilding) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const errors = validateNodes(reactFlowInstance!);
|
const errors = validateNodes(
|
||||||
|
reactFlowInstance!.getNodes(),
|
||||||
|
reactFlowInstance!.getEdges()
|
||||||
|
);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
setErrorData({
|
setErrorData({
|
||||||
title: "Oops! Looks like you missed something",
|
title: "Oops! Looks like you missed something",
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,7 @@ const EditNodeModal = forwardRef(
|
||||||
</div>
|
</div>
|
||||||
) : myData.current.node?.template[templateParam]
|
) : myData.current.node?.template[templateParam]
|
||||||
.type === "dict" ? (
|
.type === "dict" ? (
|
||||||
<div className="mt-2 w-full max-h-48 overflow-auto custom-scroll">
|
<div className="mt-2 max-h-48 w-full overflow-auto custom-scroll">
|
||||||
<KeypairListComponent
|
<KeypairListComponent
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
editNode={true}
|
editNode={true}
|
||||||
|
|
|
||||||
|
|
@ -356,7 +356,10 @@ export default function FormModal({
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
function sendMessage(): void {
|
function sendMessage(): void {
|
||||||
let nodeValidationErrors = validateNodes(reactFlowInstance!);
|
let nodeValidationErrors = validateNodes(
|
||||||
|
reactFlowInstance!.getNodes(),
|
||||||
|
reactFlowInstance!.getEdges()
|
||||||
|
);
|
||||||
if (nodeValidationErrors.length === 0) {
|
if (nodeValidationErrors.length === 0) {
|
||||||
setLockChat(true);
|
setLockChat(true);
|
||||||
let inputs = tabsState[id.current].formKeysData.input_keys;
|
let inputs = tabsState[id.current].formKeysData.input_keys;
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export default function NodeToolbarComponent({
|
||||||
setShowNode((prev) => !prev);
|
setShowNode((prev) => !prev);
|
||||||
updateNodeInternals(data.id);
|
updateNodeInternals(data.id);
|
||||||
}
|
}
|
||||||
if(event.includes("disabled")){
|
if (event.includes("disabled")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -156,8 +156,20 @@ export default function NodeToolbarComponent({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value={getRandomKeyByssmm() + (nodeLength==0?"disabled":"advanced")}>
|
<SelectItem
|
||||||
<div className={"flex "+(nodeLength==0?"text-muted-foreground":"text-primary")}>
|
value={
|
||||||
|
getRandomKeyByssmm() +
|
||||||
|
(nodeLength == 0 ? "disabled" : "advanced")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"flex " +
|
||||||
|
(nodeLength == 0
|
||||||
|
? "text-muted-foreground"
|
||||||
|
: "text-primary")
|
||||||
|
}
|
||||||
|
>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="Settings2"
|
name="Settings2"
|
||||||
className="relative top-0.5 mr-2 h-4 w-4"
|
className="relative top-0.5 mr-2 h-4 w-4"
|
||||||
|
|
@ -182,7 +194,7 @@ export default function NodeToolbarComponent({
|
||||||
<ShadTooltip content="Edit" side="top">
|
<ShadTooltip content="Edit" side="top">
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
disabled={nodeLength === 0}
|
disabled={nodeLength === 0}
|
||||||
onClick={() => setShowModalAdvanced(true)}
|
onClick={() => setShowModalAdvanced(true)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"relative -ml-px inline-flex items-center rounded-r-md bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
|
"relative -ml-px inline-flex items-center rounded-r-md bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,13 @@ export default function HomePage(): JSX.Element {
|
||||||
|
|
||||||
const dragOver = (e) => {
|
const dragOver = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if(e.dataTransfer.types.some((types) => types === "Files")){
|
if (e.dataTransfer.types.some((types) => types === "Files")) {
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const dragEnter = (e) => {
|
const dragEnter = (e) => {
|
||||||
if(e.dataTransfer.types.some((types) => types === "Files")){
|
if (e.dataTransfer.types.some((types) => types === "Files")) {
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
||||||
|
|
@ -134,18 +134,18 @@ export type TooltipComponentType = {
|
||||||
children: ReactElement;
|
children: ReactElement;
|
||||||
title: string | ReactElement;
|
title: string | ReactElement;
|
||||||
placement?:
|
placement?:
|
||||||
| "bottom-end"
|
| "bottom-end"
|
||||||
| "bottom-start"
|
| "bottom-start"
|
||||||
| "bottom"
|
| "bottom"
|
||||||
| "left-end"
|
| "left-end"
|
||||||
| "left-start"
|
| "left-start"
|
||||||
| "left"
|
| "left"
|
||||||
| "right-end"
|
| "right-end"
|
||||||
| "right-start"
|
| "right-start"
|
||||||
| "right"
|
| "right"
|
||||||
| "top-end"
|
| "top-end"
|
||||||
| "top-start"
|
| "top-start"
|
||||||
| "top";
|
| "top";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProgressBarType = {
|
export type ProgressBarType = {
|
||||||
|
|
|
||||||
|
|
@ -189,10 +189,7 @@ export function buildTweaks(flow: FlowType) {
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateNode(
|
export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
|
||||||
node: NodeType,
|
|
||||||
reactFlowInstance: ReactFlowInstance
|
|
||||||
): Array<string> {
|
|
||||||
if (!node.data?.node?.template || !Object.keys(node.data.node.template)) {
|
if (!node.data?.node?.template || !Object.keys(node.data.node.template)) {
|
||||||
return [
|
return [
|
||||||
"We've noticed a potential issue with a node in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
|
"We've noticed a potential issue with a node in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
|
||||||
|
|
@ -211,13 +208,11 @@ export function validateNode(
|
||||||
(template[t].value === undefined ||
|
(template[t].value === undefined ||
|
||||||
template[t].value === null ||
|
template[t].value === null ||
|
||||||
template[t].value === "") &&
|
template[t].value === "") &&
|
||||||
!reactFlowInstance
|
!edges.some(
|
||||||
.getEdges()
|
(edge) =>
|
||||||
.some(
|
edge.targetHandle?.split("|")[1] === t &&
|
||||||
(edge) =>
|
edge.targetHandle.split("|")[2] === node.id
|
||||||
edge.targetHandle?.split("|")[1] === t &&
|
)
|
||||||
edge.targetHandle.split("|")[2] === node.id
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
errors.push(
|
errors.push(
|
||||||
`${type} is missing ${
|
`${type} is missing ${
|
||||||
|
|
@ -249,15 +244,13 @@ export function validateNode(
|
||||||
}, [] as string[]);
|
}, [] as string[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateNodes(reactFlowInstance: ReactFlowInstance) {
|
export function validateNodes(nodes: Node[], edges: Edge[]) {
|
||||||
if (reactFlowInstance.getNodes().length === 0) {
|
if (nodes.length === 0) {
|
||||||
return [
|
return [
|
||||||
"No nodes found in the flow. Please add at least one node to the flow.",
|
"No nodes found in the flow. Please add at least one node to the flow.",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return reactFlowInstance
|
return nodes.flatMap((n: NodeType) => validateNode(n, edges));
|
||||||
.getNodes()
|
|
||||||
.flatMap((n: NodeType) => validateNode(n, reactFlowInstance));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
|
export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
|
||||||
|
|
@ -311,15 +304,20 @@ export function getConnectedNodes(
|
||||||
return nodes.filter((node) => node.id === targetId || node.id === sourceId);
|
return nodes.filter((node) => node.id === targetId || node.id === sourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertObjToArray(singleObject) {
|
export function convertObjToArray(singleObject: object | string) {
|
||||||
|
if (typeof singleObject === "string") {
|
||||||
|
singleObject = JSON.parse(singleObject);
|
||||||
|
}
|
||||||
if (Array.isArray(singleObject)) return singleObject;
|
if (Array.isArray(singleObject)) return singleObject;
|
||||||
|
|
||||||
let arrConverted: any = [];
|
let arrConverted: any[] = [];
|
||||||
for (const key in singleObject) {
|
if (typeof singleObject === "object") {
|
||||||
if (singleObject.hasOwnProperty(key)) {
|
for (const key in singleObject) {
|
||||||
const newObj = {};
|
if (Object.prototype.hasOwnProperty.call(singleObject, key)) {
|
||||||
newObj[key] = singleObject[key];
|
const newObj = {};
|
||||||
arrConverted.push(newObj);
|
newObj[key] = singleObject[key];
|
||||||
|
arrConverted.push(newObj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return arrConverted;
|
return arrConverted;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ class NameTest(FastHttpUser):
|
||||||
while True:
|
while True:
|
||||||
with self.rest(
|
with self.rest(
|
||||||
"GET",
|
"GET",
|
||||||
f"/task/{task_id}/status",
|
f"/task/{task_id}",
|
||||||
name="task_status",
|
name="task_status",
|
||||||
headers=self.headers,
|
headers=self.headers,
|
||||||
) as response:
|
) as response:
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,10 @@ def run_post(client, flow_id, headers, post_data):
|
||||||
|
|
||||||
|
|
||||||
# Helper function to poll task status
|
# Helper function to poll task status
|
||||||
def poll_task_status(client, headers, task_id, max_attempts=20, sleep_time=1):
|
def poll_task_status(client, headers, href, max_attempts=20, sleep_time=1):
|
||||||
for _ in range(max_attempts):
|
for _ in range(max_attempts):
|
||||||
task_status_response = client.get(
|
task_status_response = client.get(
|
||||||
f"api/v1/task/{task_id}/status",
|
href,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
|
|
@ -544,11 +544,15 @@ def test_async_task_processing(distributed_client, added_flow, created_api_key):
|
||||||
assert response.status_code == 200, response.json()
|
assert response.status_code == 200, response.json()
|
||||||
|
|
||||||
# Extract the task ID from the response
|
# Extract the task ID from the response
|
||||||
task_id = response.json().get("id")
|
task = response.json().get("task")
|
||||||
|
task_id = task.get("id")
|
||||||
|
task_href = task.get("href")
|
||||||
assert task_id is not None
|
assert task_id is not None
|
||||||
|
assert task_href is not None
|
||||||
|
assert task_href == f"api/v1/task/{task_id}"
|
||||||
|
|
||||||
# Polling the task status using the helper function
|
# Polling the task status using the helper function
|
||||||
task_status_json = poll_task_status(distributed_client, headers, task_id)
|
task_status_json = poll_task_status(distributed_client, headers, task_href)
|
||||||
assert task_status_json is not None, "Task did not complete in time"
|
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
|
# Validate that the task completed successfully and the result is as expected
|
||||||
|
|
@ -576,11 +580,15 @@ def test_async_task_processing_vector_store(
|
||||||
assert "FAILURE" not in response.json()["result"]
|
assert "FAILURE" not in response.json()["result"]
|
||||||
|
|
||||||
# Extract the task ID from the response
|
# Extract the task ID from the response
|
||||||
task_id = response.json().get("id")
|
task = response.json().get("task")
|
||||||
|
task_id = task.get("id")
|
||||||
|
task_href = task.get("href")
|
||||||
assert task_id is not None
|
assert task_id is not None
|
||||||
|
assert task_href is not None
|
||||||
|
assert task_href == f"api/v1/task/{task_id}"
|
||||||
|
|
||||||
# Polling the task status using the helper function
|
# Polling the task status using the helper function
|
||||||
task_status_json = poll_task_status(client, headers, task_id, max_attempts=40)
|
task_status_json = poll_task_status(client, headers, task_href)
|
||||||
assert task_status_json is not None, "Task did not complete in time"
|
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
|
# Validate that the task completed successfully and the result is as expected
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue