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/guidelines/async-api.mdx b/docs/docs/guidelines/async-api.mdx index 582a5e1c8..c5473812e 100644 --- a/docs/docs/guidelines/async-api.mdx +++ b/docs/docs/guidelines/async-api.mdx @@ -27,13 +27,29 @@ 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}/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/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 ``` 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