From af35ae315e8843dbb2c617e789d983b92627fe88 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 26 Sep 2023 19:34:50 -0300 Subject: [PATCH 01/17] =?UTF-8?q?=F0=9F=93=9D=20docs(async-api.mdx):=20upd?= =?UTF-8?q?ate=20endpoint=20path=20for=20checking=20task=20status=20to=20i?= =?UTF-8?q?mprove=20consistency=20and=20clarity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🐛 fix(endpoints.py): update endpoint path for checking task status to match the updated path in the documentation 🐛 fix(schemas.py): add TaskResponse schema to properly handle task response data 🐛 fix(locustfile.py): update endpoint path for polling task status to match the updated path in the endpoints 🐛 fix(test_endpoints.py): update helper function and test cases to use the new task response structure and endpoint path for polling task status --- docs/docs/guidelines/async-api.mdx | 4 ++-- src/backend/langflow/api/v1/endpoints.py | 11 +++++++++-- src/backend/langflow/api/v1/schemas.py | 9 ++++++++- tests/locust/locustfile.py | 2 +- tests/test_endpoints.py | 20 ++++++++++++++------ 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/docs/docs/guidelines/async-api.mdx b/docs/docs/guidelines/async-api.mdx index 582a5e1c8..9ef836704 100644 --- a/docs/docs/guidelines/async-api.mdx +++ b/docs/docs/guidelines/async-api.mdx @@ -29,11 +29,11 @@ curl -X POST \ ## 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 curl -X GET \ - http://localhost:3000/api/v1/task//status \ + http://localhost:3000/api/v1/task/ \ -H 'x-api-key: ' ``` diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index ff5e0541e..864467f9b 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -20,6 +20,7 @@ from langflow.interface.custom.custom_component import CustomComponent from langflow.api.v1.schemas import ( ProcessResponse, + TaskResponse, TaskStatusResponse, UploadFileResponse, CustomComponentCode, @@ -145,9 +146,15 @@ async def process_flow( session_id, ) 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( result=task_result, - id=task_id, + task=task_response, session_id=session_id, backend=str(type(task_service.backend)), ) @@ -173,7 +180,7 @@ async def process_flow( 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): task_service = get_task_service() task = task_service.get_task(task_id) diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index 519bc534e..9c6ac6d60 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -47,11 +47,18 @@ class UpdateTemplateRequest(BaseModel): template: dict +class TaskResponse(BaseModel): + """Task response schema.""" + + id: Optional[str] = Field(None) + href: Optional[str] = Field(None) + + class ProcessResponse(BaseModel): """Process response schema.""" result: Any - id: Optional[str] = None + task: Optional[TaskResponse] = None session_id: Optional[str] = None backend: Optional[str] = None diff --git a/tests/locust/locustfile.py b/tests/locust/locustfile.py index 000c3456d..aca0d1de9 100644 --- a/tests/locust/locustfile.py +++ b/tests/locust/locustfile.py @@ -19,7 +19,7 @@ class NameTest(FastHttpUser): while True: with self.rest( "GET", - f"/task/{task_id}/status", + f"/task/{task_id}", name="task_status", headers=self.headers, ) as response: diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 43a68580a..1de7c9deb 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -25,10 +25,10 @@ def run_post(client, flow_id, headers, post_data): # 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): task_status_response = client.get( - f"api/v1/task/{task_id}/status", + href, headers=headers, ) if ( @@ -544,11 +544,15 @@ def test_async_task_processing(distributed_client, added_flow, created_api_key): assert response.status_code == 200, response.json() # 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_href is not None + assert task_href == f"api/v1/task/{task_id}" # 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" # 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"] # 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_href is not None + assert task_href == f"api/v1/task/{task_id}" # 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" # Validate that the task completed successfully and the result is as expected From 868515958e2e60d6e714a0086bb4eb89ece5fc58 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 26 Sep 2023 19:45:08 -0300 Subject: [PATCH 02/17] =?UTF-8?q?=F0=9F=93=9D=20docs(async-api.mdx):=20add?= =?UTF-8?q?=20response=20example=20for=20async=20API=20request=20to=20impr?= =?UTF-8?q?ove=20documentation=20clarity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs/guidelines/async-api.mdx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/docs/guidelines/async-api.mdx b/docs/docs/guidelines/async-api.mdx index 9ef836704..c5473812e 100644 --- a/docs/docs/guidelines/async-api.mdx +++ b/docs/docs/guidelines/async-api.mdx @@ -27,6 +27,22 @@ curl -X POST \ -d '{"inputs": {"text": ""}, "tweaks": {}, "sync": false}' ``` +Response: + +```json +{ + "result": { + "output": "..." + }, + "task": { + "id": "...", + "href": "api/v1/task/" + }, + "session_id": "...", + "backend": "..." // celery or anyio +} +``` + ## Checking Task Status You can check the status of an asynchronous task by making a GET request to the `/task/{task_id}` endpoint. From f506fd16253e96fbb5cb144881573fcceb5e86bb Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 26 Sep 2023 19:58:50 -0300 Subject: [PATCH 03/17] =?UTF-8?q?=F0=9F=94=A5=20chore(deploy):=20remove=20?= =?UTF-8?q?unused=20docker-compose.test.yml=20file=20=F0=9F=94=A7=20chore(?= =?UTF-8?q?deploy):=20update=20docker-compose.override.yml=20to=20version?= =?UTF-8?q?=203.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📦 chore(docker-compose.with_tests.yml): add docker-compose file with tests configuration This commit adds a new docker-compose file named `docker-compose.with_tests.yml` which includes the configuration for running tests. The file includes the following services: - `proxy`: Configures Traefik as a reverse proxy with Docker integration and enables access logs, the Traefik dashboard, and API. - `backend`: Sets up the backend service with dependencies on a database, message broker, and result backend. It also includes labels for Traefik routing. - `db`: Configures a PostgreSQL database with a volume for data persistence. - `pgadmin`: Sets up pgAdmin for managing the PostgreSQL database. - `result_backend`: Configures a Redis instance for the result backend. - `celeryworker`: Sets up a Celery worker for background task processing. - `flower`: Configures Flower for monitoring and managing Celery workers. - `frontend`: Sets up the frontend service with labels for Traefik routing. - `broker`: Configures RabbitMQ with the management console. - `prometheus`: Sets up Prometheus for monitoring. - `grafana`: Configures Grafana for visualization and monitoring. - `tests`: Extends the `backend` service and runs pytest for running tests. This file allows running the application with the necessary services for testing and monitoring. 🔧 chore(docker-compose.yml): add missing volumes and networks for services 🔧 chore(docker-compose.yml): add traefik-public network with configurable external setting for flexibility in testing 📝 docs(async-tasks.mdx): update docker-compose command to use the correct file name for running tests --- deploy/docker-compose.override.yml | 1 + deploy/docker-compose.test.yml | 85 -------- deploy/docker-compose.with_tests.yml | 277 +++++++++++++++++++++++++++ docs/docs/guides/async-tasks.mdx | 2 +- 4 files changed, 279 insertions(+), 86 deletions(-) delete mode 100644 deploy/docker-compose.test.yml create mode 100644 deploy/docker-compose.with_tests.yml diff --git a/deploy/docker-compose.override.yml b/deploy/docker-compose.override.yml index e5c0d9dd7..1992916fb 100644 --- a/deploy/docker-compose.override.yml +++ b/deploy/docker-compose.override.yml @@ -1,4 +1,5 @@ version: "3.8" + services: proxy: ports: diff --git a/deploy/docker-compose.test.yml b/deploy/docker-compose.test.yml deleted file mode 100644 index 829aae883..000000000 --- a/deploy/docker-compose.test.yml +++ /dev/null @@ -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 diff --git a/deploy/docker-compose.with_tests.yml b/deploy/docker-compose.with_tests.yml new file mode 100644 index 000000000..c16c14197 --- /dev/null +++ b/deploy/docker-compose.with_tests.yml @@ -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} diff --git a/docs/docs/guides/async-tasks.mdx b/docs/docs/guides/async-tasks.mdx index 5cccc8675..e865fe4b6 100644 --- a/docs/docs/guides/async-tasks.mdx +++ b/docs/docs/guides/async-tasks.mdx @@ -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: ```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 ``` From 5ec118cc6f460a5250a62e725539924c737fc64e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 26 Sep 2023 20:06:38 -0300 Subject: [PATCH 04/17] =?UTF-8?q?=F0=9F=90=9B=20fix(custom=5Fcomponent.py)?= =?UTF-8?q?:=20improve=20error=20message=20formatting=20for=20type=20hint?= =?UTF-8?q?=20error=20in=20build=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The error message for a type hint error in the build method was improved to provide clearer instructions. The traceback message was updated to suggest using PromptTemplate instead of Prompt type. This change improves the readability and usability of the error message. --- src/backend/langflow/interface/custom/custom_component.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/interface/custom/custom_component.py b/src/backend/langflow/interface/custom/custom_component.py index b0c4f8752..ccc0b08b2 100644 --- a/src/backend/langflow/interface/custom/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component.py @@ -102,7 +102,10 @@ class CustomComponent(Component, extra=Extra.allow): status_code=400, detail={ "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 From 0b535c54f540b828a53c6301ffdcd15eba2bb09d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 27 Sep 2023 08:48:21 -0300 Subject: [PATCH 05/17] =?UTF-8?q?=F0=9F=94=A7=20chore(=5F=5Fmain=5F=5F.py)?= =?UTF-8?q?:=20update=20Typer=20configuration=20to=20treat=20no=20argument?= =?UTF-8?q?s=20as=20help=20to=20improve=20usability=20=F0=9F=94=A7=20chore?= =?UTF-8?q?(=5F=5Fmain=5F=5F.py):=20update=20docstring=20for=20run()=20fun?= =?UTF-8?q?ction=20to=20improve=20clarity=20=F0=9F=94=A7=20chore(=5F=5Fmai?= =?UTF-8?q?n=5F=5F.py):=20update=20docstring=20for=20superuser()=20functio?= =?UTF-8?q?n=20to=20improve=20clarity=20=F0=9F=94=A7=20chore(=5F=5Fmain=5F?= =?UTF-8?q?=5F.py):=20update=20migration()=20function=20to=20default=20to?= =?UTF-8?q?=20test=20mode=20=F0=9F=94=A7=20chore(manager.py):=20remove=20u?= =?UTF-8?q?nnecessary=20logging=20of=20database=20URL=20in=20run=5Fmigrati?= =?UTF-8?q?ons()=20function=20=F0=9F=94=A7=20chore(manager.py):=20add=20mi?= =?UTF-8?q?ssing=20import=20statement=20for=20logger=20in=20check=5Fcelery?= =?UTF-8?q?=5Favailability()=20function=20=F0=9F=94=A7=20chore(manager.py)?= =?UTF-8?q?:=20update=20configure()=20function=20to=20be=20called=20in=20c?= =?UTF-8?q?heck=5Fcelery=5Favailability()=20function=20=F0=9F=94=A7=20chor?= =?UTF-8?q?e(logger.py):=20update=20configure()=20function=20to=20use=20en?= =?UTF-8?q?vironment=20variable=20for=20log=20level=20if=20available=20?= =?UTF-8?q?=F0=9F=94=A7=20chore(logger.py):=20update=20configure()=20funct?= =?UTF-8?q?ion=20to=20use=20default=20log=20file=20location=20if=20not=20p?= =?UTF-8?q?rovided?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/__main__.py | 16 ++++-- .../langflow/services/database/manager.py | 4 +- src/backend/langflow/services/task/manager.py | 10 ++-- src/backend/langflow/utils/logger.py | 52 ++++++++++++++----- 4 files changed, 59 insertions(+), 23 deletions(-) diff --git a/src/backend/langflow/__main__.py b/src/backend/langflow/__main__.py index 6701d27bc..f42f44a9e 100644 --- a/src/backend/langflow/__main__.py +++ b/src/backend/langflow/__main__.py @@ -23,7 +23,7 @@ from rich.table import Table console = Console() -app = typer.Typer() +app = typer.Typer(no_args_is_help=True) 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 if env_file: @@ -299,7 +299,14 @@ def superuser( password: str = typer.Option( ..., 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() db_service = get_db_service() with session_getter(db_service) as session: @@ -321,7 +328,10 @@ def superuser( @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() db_service = get_db_service() if not test: diff --git a/src/backend/langflow/services/database/manager.py b/src/backend/langflow/services/database/manager.py index ca7f34d10..3a388f694 100644 --- a/src/backend/langflow/services/database/manager.py +++ b/src/backend/langflow/services/database/manager.py @@ -94,9 +94,7 @@ class DatabaseService(Service): return True def run_migrations(self): - logger.info( - f"Running DB migrations in {self.script_location} on {self.database_url}" - ) + logger.info(f"Running DB migrations in {self.script_location}") alembic_cfg = Config() alembic_cfg.set_main_option("script_location", str(self.script_location)) alembic_cfg.set_main_option("sqlalchemy.url", self.database_url) diff --git a/src/backend/langflow/services/task/manager.py b/src/backend/langflow/services/task/manager.py index ab74d916e..422b34faa 100644 --- a/src/backend/langflow/services/task/manager.py +++ b/src/backend/langflow/services/task/manager.py @@ -1,4 +1,5 @@ from typing import Any, Callable, Coroutine, Union +from langflow.utils.logger import configure from loguru import logger from langflow.services.base import Service 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(): - from langflow.worker import celery_app - try: + from langflow.worker import celery_app + status = get_celery_worker_status(celery_app) logger.debug(f"Celery status: {status}") - except Exception as e: - logger.error(f"An error occurred: {e}") + except Exception as exc: + logger.debug(f"Celery not available: {exc}") status = {"availability": None} return status try: + configure() status = check_celery_availability() USE_CELERY = status.get("availability") is not None diff --git a/src/backend/langflow/utils/logger.py b/src/backend/langflow/utils/logger.py index 1f616486b..05b7adc0e 100644 --- a/src/backend/langflow/utils/logger.py +++ b/src/backend/langflow/utils/logger.py @@ -2,12 +2,34 @@ from typing import Optional from loguru import logger from pathlib import Path from rich.logging import RichHandler +import os +import orjson +import appdirs -def configure(log_level: str = "DEBUG", log_file: Optional[Path] = None): - log_format = "{time:HH:mm:ss} - {level: <8} - {message}" +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: str = "INFO", log_file: Optional[Path] = None): + if os.getenv("LANGFLOW_LOG_LEVEL"): + log_level = os.getenv("LANGFLOW_LOG_LEVEL") + # Human-readable + log_format = "{time:YYYY-MM-DD HH:mm:ss} - {level: <8} - {module} - {message}" + + # log_format = log_format_dev if log_level.upper() == "DEBUG" else log_format_prod logger.remove() # Remove default handlers - + logger.patch(patching) # Configure loguru to use RichHandler logger.configure( handlers=[ @@ -19,17 +41,21 @@ def configure(log_level: str = "DEBUG", log_file: Optional[Path] = None): ] ) - if log_file: - log_file = Path(log_file) - log_file.parent.mkdir(parents=True, exist_ok=True) + if not log_file: + cache_dir = Path(appdirs.user_cache_dir("langflow")) + log_file = cache_dir / "langflow.log" - logger.add( - sink=str(log_file), - level=log_level.upper(), - format=log_format, - rotation="10 MB", # Log rotation based on file size - ) + log_file = Path(log_file) + log_file.parent.mkdir(parents=True, exist_ok=True) - 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: logger.info(f"Log file: {log_file}") From 0b2b074e07c8da32f1eb9f6d968eb80e98519ada Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 27 Sep 2023 08:51:17 -0300 Subject: [PATCH 06/17] =?UTF-8?q?=F0=9F=94=A7=20chore(logger.py):=20refact?= =?UTF-8?q?or=20configure=20function=20to=20improve=20readability=20and=20?= =?UTF-8?q?add=20support=20for=20configurable=20log=20level=20=F0=9F=94=92?= =?UTF-8?q?=20chore(logger.py):=20add=20VALID=5FLOG=5FLEVELS=20constant=20?= =?UTF-8?q?to=20define=20valid=20log=20levels=20for=20better=20maintainabi?= =?UTF-8?q?lity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/utils/logger.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/utils/logger.py b/src/backend/langflow/utils/logger.py index 05b7adc0e..e49fa4bfd 100644 --- a/src/backend/langflow/utils/logger.py +++ b/src/backend/langflow/utils/logger.py @@ -7,6 +7,9 @@ import orjson import appdirs +VALID_LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + + def serialize(record): subset = { "timestamp": record["time"].timestamp(), @@ -21,9 +24,11 @@ def patching(record): record["extra"]["serialized"] = serialize(record) -def configure(log_level: str = "INFO", log_file: Optional[Path] = None): - if os.getenv("LANGFLOW_LOG_LEVEL"): +def configure(log_level: Optional[str] = None, log_file: Optional[Path] = None): + if os.getenv("LANGFLOW_LOG_LEVEL") in VALID_LOG_LEVELS and not log_level: log_level = os.getenv("LANGFLOW_LOG_LEVEL") + elif not log_level: + log_level = "INFO" # Human-readable log_format = "{time:YYYY-MM-DD HH:mm:ss} - {level: <8} - {module} - {message}" From ede745c1ddc36b27e8298e3ce598467369b63f50 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 27 Sep 2023 08:54:11 -0300 Subject: [PATCH 07/17] =?UTF-8?q?=F0=9F=90=9B=20fix(custom=5Fcomponent.py)?= =?UTF-8?q?:=20improve=20error=20message=20formatting=20for=20type=20hint?= =?UTF-8?q?=20error=20in=20build=20method=20=F0=9F=90=9B=20fix(logger.py):?= =?UTF-8?q?=20fix=20conditional=20statement=20for=20log=5Flevel=20configur?= =?UTF-8?q?ation=20to=20properly=20handle=20None=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../langflow/interface/custom/custom_component.py | 5 ++++- src/backend/langflow/utils/logger.py | 10 +++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/backend/langflow/interface/custom/custom_component.py b/src/backend/langflow/interface/custom/custom_component.py index b0c4f8752..ccc0b08b2 100644 --- a/src/backend/langflow/interface/custom/custom_component.py +++ b/src/backend/langflow/interface/custom/custom_component.py @@ -102,7 +102,10 @@ class CustomComponent(Component, extra=Extra.allow): status_code=400, detail={ "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 diff --git a/src/backend/langflow/utils/logger.py b/src/backend/langflow/utils/logger.py index e49fa4bfd..ebc468e7e 100644 --- a/src/backend/langflow/utils/logger.py +++ b/src/backend/langflow/utils/logger.py @@ -1,3 +1,4 @@ +from math import log from typing import Optional from loguru import logger from pathlib import Path @@ -25,12 +26,15 @@ def patching(record): def configure(log_level: Optional[str] = None, log_file: Optional[Path] = None): - if os.getenv("LANGFLOW_LOG_LEVEL") in VALID_LOG_LEVELS and not log_level: + if os.getenv("LANGFLOW_LOG_LEVEL") in VALID_LOG_LEVELS and log_level is None: log_level = os.getenv("LANGFLOW_LOG_LEVEL") - elif not log_level: + if log_level is None: log_level = "INFO" # Human-readable - log_format = "{time:YYYY-MM-DD HH:mm:ss} - {level: <8} - {module} - {message}" + log_format = ( + "{time:YYYY-MM-DD HH:mm:ss} - " + "{level: <8} - {module} - {message}" + ) # log_format = log_format_dev if log_level.upper() == "DEBUG" else log_format_prod logger.remove() # Remove default handlers From 05c73d4664a1fd0c157e308b2d7a88f590971833 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 27 Sep 2023 09:10:42 -0300 Subject: [PATCH 08/17] =?UTF-8?q?=F0=9F=94=A7=20chore(Makefile):=20remove?= =?UTF-8?q?=20unnecessary=20parallel=20test=20execution=20flag=20to=20simp?= =?UTF-8?q?lify=20test=20command=20=F0=9F=94=A7=20chore(Makefile):=20remov?= =?UTF-8?q?e=20unnecessary=20parallel=20test=20execution=20flag=20to=20sim?= =?UTF-8?q?plify=20test=20command=20=F0=9F=94=A7=20chore(Makefile):=20remo?= =?UTF-8?q?ve=20unnecessary=20parallel=20test=20execution=20flag=20to=20si?= =?UTF-8?q?mplify=20test=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 32198730f..1fbd7403c 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ coverage: tests: @make install_backend - poetry run pytest tests -n auto + poetry run pytest tests format: poetry run black . From 76def20d86f0a774caee2b74a3fe61489eb797c0 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 27 Sep 2023 10:19:49 -0300 Subject: [PATCH 09/17] fix bug when object comes as a string from backend --- .../src/CustomNodes/GenericNode/index.tsx | 1 - src/frontend/src/utils/reactflowUtils.ts | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 5f3a99d47..524b54ead 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -107,7 +107,6 @@ export default function GenericNode({ setValidationStatus(null); } }, [sseData, data.id]); - return ( <> diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index a9b157c66..4b431cb22 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -311,15 +311,20 @@ export function getConnectedNodes( 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; - let arrConverted: any = []; - for (const key in singleObject) { - if (singleObject.hasOwnProperty(key)) { - const newObj = {}; - newObj[key] = singleObject[key]; - arrConverted.push(newObj); + let arrConverted: any[] = []; + if (typeof singleObject === "object") { + for (const key in singleObject) { + if (Object.prototype.hasOwnProperty.call(singleObject, key)) { + const newObj = {}; + newObj[key] = singleObject[key]; + arrConverted.push(newObj); + } } } return arrConverted; From eaab405742b9d673ccee1d713841db9d7d9f9da9 Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Wed, 27 Sep 2023 10:20:34 -0300 Subject: [PATCH 10/17] code format --- src/backend/langflow/utils/logger.py | 1 - .../src/modals/EditNodeModal/index.tsx | 2 +- .../components/nodeToolbarComponent/index.tsx | 20 ++++++++++++---- src/frontend/src/pages/MainPage/index.tsx | 4 ++-- src/frontend/src/types/components/index.ts | 24 +++++++++---------- 5 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/backend/langflow/utils/logger.py b/src/backend/langflow/utils/logger.py index ebc468e7e..b08621410 100644 --- a/src/backend/langflow/utils/logger.py +++ b/src/backend/langflow/utils/logger.py @@ -1,4 +1,3 @@ -from math import log from typing import Optional from loguru import logger from pathlib import Path diff --git a/src/frontend/src/modals/EditNodeModal/index.tsx b/src/frontend/src/modals/EditNodeModal/index.tsx index 02753cc44..6d6c5cfda 100644 --- a/src/frontend/src/modals/EditNodeModal/index.tsx +++ b/src/frontend/src/modals/EditNodeModal/index.tsx @@ -256,7 +256,7 @@ const EditNodeModal = forwardRef( ) : myData.current.node?.template[templateParam] .type === "dict" ? ( -
+
!prev); updateNodeInternals(data.id); } - if(event.includes("disabled")){ + if (event.includes("disabled")) { return; } }; @@ -156,8 +156,20 @@ export default function NodeToolbarComponent({ - -
+ +