Files
langflow/pyproject.toml
Jordan Frazier 71cd5ce376 feat: improve lfx serve production-readiness (#13121)
* docs: add design spec for lfx serve multi-flow startup and dynamic upload

* docs: add implementation plan for lfx serve multi-flow startup and dynamic upload

* feat(serve): add FlowRegistry for mutable in-process flow management

* feat(serve): add flow_id_from_content for content-based flow ID generation

* fix(test): update streaming tests to use FlowRegistry API

* test(serve): add upload endpoint tests

* feat(serve): add build_registry_from_directory and build_registry_from_paths helpers

* refactor(serve): remove dead _analyze_graph_structure and _generate_dynamic_run_description helpers

* feat(serve): accept directory and multiple file paths at startup; wire registry to serve_command

* [autofix.ci] apply automated fixes

* fix(serve): move traceback import to module level; remove planning docs from branch

* [autofix.ci] apply automated fixes

* fix(serve): deepcopy graph in stream_flow; use json.dumps for error stream payload

* test(serve): verify stream setup error payload is valid JSON with special characters

* [autofix.ci] apply automated fixes

* fix(serve): use load_flow_from_json directly in _load_graph_and_meta for clear error messages

* docs(serve): document multi-worker and persistence limitations on FlowRegistry

* feat(serve): allow starting server with no flows; accept full Langflow flow JSON on upload

- Remove the requirement for a flow path/JSON/stdin at startup; lfx serve
  now starts with an empty FlowRegistry when no input is provided, ready
  to accept flows via POST /flows/upload/
- UploadFlowRequest now accepts the full Langflow export JSON directly
  (extra="allow", id field added) so callers can do -d @flow.json without
  wrapping; load_flow_from_json receives the full dict and the flow's own
  stable id is used when present

* fix(serve): harden upload endpoint and remove unused verbose_print param

- Pass body.model_dump(exclude={"replace"}) to load_flow_from_json instead of
  body.model_dump(); keeps the top-level "data" key the function expects while
  excluding the internal replace flag
- Set graph.flow_id after upload so it matches the registry key
- Return 409 Conflict on duplicate flow ID; add replace=true field to allow
  explicit overwrite
- Remove unused verbose_print param from _load_graph_and_meta and update callers
- Clarify directory scan is top-level only (non-recursive) in CLI help text
- Add test_upload_full_export_as_body regression test: sends a complete Langflow
  export JSON as the request body (curl -d @flow.json pattern) where body.data
  has no nested "data" key — would have caught the KeyError('data') regression

* fix(serve): address PR review comments

- Restore .py script support: _load_graph_and_meta now dispatches to
  load_graph_from_script for .py paths; CLI validation accepts .json and .py;
  help text updated in both _running_commands.py and commands.py
- Fix flow ID collision in build_registry_from_paths: compute a shared common
  root from all supplied paths so same-named files in different directories
  get distinct UUIDv5 IDs instead of silently overwriting each other
- Harden FlowRegistry.add(): add overwrite=False parameter that raises
  ValueError on duplicate IDs; startup paths keep default (collision = error),
  upload endpoint passes overwrite=body.replace
- Add regression test: test_same_filename_in_different_dirs_gets_distinct_ids
- Update test_duplicate_add_replaces_graph to reflect new overwrite semantics

* test(serve): add unit tests for .py script path in serve command

- test_load_graph_and_meta_dispatches_to_script_loader_for_py: verifies
  _load_graph_and_meta calls load_graph_from_script (not load_flow_from_json)
  for .py paths, and that meta.title and relative_path are set correctly
- test_serve_command_accepts_py_file: end-to-end check that lfx serve my.py
  reaches the server (exit_code == 0)
- test_serve_command_rejects_unsupported_extension: .txt and other unknown
  extensions must exit non-zero with a helpful message

* [autofix.ci] apply automated fixes

* Add --no-env-fallback option to support scoped request_vars

* [autofix.ci] apply automated fixes

* feat(serve): filesystem-backed flow registry for multi-worker sharing

Adds a FlowStore abstraction with two implementations:
- NullFlowStore (default): in-memory only, existing behavior unchanged
- FilesystemFlowStore: persists each flow as {id}.json; writes are
  atomic (tmp→rename); reads and deletes are TOCTOU-safe; path
  traversal guard rejects unsafe flow IDs

FlowRegistry now delegates persistence to FlowStore:
- add(raw_json=...) writes to store when raw JSON is available
- get() falls through to store on cache miss, reconstructs + caches
- list_metas() merges in-memory and store-side entries
- remove() propagates deletion to store
- warm_from_store() pre-warms the in-memory cache at startup

New --flow-dir option on lfx serve activates the filesystem store.
All uvicorn workers pointing at the same directory serve the same
flows — use /tmp/lfx-flows for single-pod sharing or a PVC mount
for cross-pod sharing. Raw JSON is plumbed through _load_graph_and_meta
and both builder functions so startup-loaded .json flows are persisted
automatically; .py flows remain in-memory only.

* feat(serve): add --workers option for multi-worker uvicorn support

Adds --workers N to lfx serve, enabling multiple uvicorn processes to
share flows via --flow-dir.

serve_command is restructured from @syncify async to plain sync: all
async registry-building is extracted into _build_serve_registry() and
called via asyncio.run(), then uvicorn.run() handles the server.

For workers > 1, uvicorn requires an import string rather than an app
object. The factory create_serve_app() in serve_app.py reads
LFX_SERVE_FLOW_DIR and LFX_SERVE_NO_ENV_FALLBACK from env, pre-warms
from the shared FilesystemFlowStore, and returns a ready FastAPI app.
The parent sets those env vars unconditionally before dispatching.

For workers == 1, the pre-built app object is used directly (no change
to existing single-worker behavior). A warning is emitted when
--workers > 1 is used without --flow-dir since each worker would have
an isolated in-memory registry.

The startup panel flow count is now accurate for single-worker +
--flow-dir by calling warm_from_store() before printing.

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* fix(serve): remove no_args_is_help, drop verbose_print param, harden _safe_path

- Remove no_args_is_help=True from serve command so lfx serve with no
  arguments starts with an empty registry instead of showing help
- Remove unused verbose_print parameter from create_multi_serve_app and
  update all callers (14 call sites across tests)
- Wrap _safe_path's relative_to() check in try/except for a clear error
  message; restructure delete() to use try/except/else (TRY300)
- Add TYPE_CHECKING guard for FlowStore annotation in commands.py; wrap
  long --workers help string; restore FBT003 noqa on no_env_fallback
- Fix test_env_fallback_skipped_when_flag_true to allow logger's getenv
  calls while still asserting the credential variable is never looked up
- Fix ARG002/D205/RUF059 lint issues in test files

* [autofix.ci] apply automated fixes

* fix(serve): resolve merge conflicts and clean up test section comments

* ruff - test updates

* fix(serve): cross-worker delete propagation and HTTP 500 for execution failures

Cross-worker stale cache (multi-worker DELETE bug):
- Add FlowStore.is_persistent to distinguish real stores from NullFlowStore
- Track _store_sourced in FlowRegistry for flows written to a persistent store
- get() re-reads the store for store-sourced flows to detect cross-worker deletes;
  evicts and returns None if the file is gone
- list_metas() skips store-sourced flows whose store key is no longer present
- Add _evict() helper to clean up _flows, _store_meta_cache, _store_keys, _store_sourced

HTTP status for execution failures:
- POST /flows/{id}/run now returns HTTP 500 (not 200) when execution raises or
  when the flow returns success=false; body is still RunResponse for debug detail
- Updated OpenAPI responses declaration: 500 now maps to RunResponse model

* fix(serve): harden multi-worker registry correctness and safety

- Add FlowAlreadyRegisteredError (ValueError subclass) so upload_flow
  catches only the specific race-condition exception rather than any
  ValueError from unrelated internals.

- Add _SERVE_ENV_PREFIX constant; rebuild LFX_SERVE_* env var names from
  it and add try/finally cleanup so those vars are always deleted after
  uvicorn.run() returns in multi-worker mode.

- Reject .py startup files when --workers > 1: Python graphs cannot be
  serialized to the filesystem store so workers would silently start with
  empty registries for those flows.

- Remove warm_from_store() from build_registry_from_paths/directory; the
  builders no longer pre-warm to avoid double-loading when the startup dir
  and flow-dir overlap. serve_command still warms for single-worker mode.

- Fix FlowRegistry.__len__ to delegate to list_metas() so store-only
  flows (not yet cache-loaded) are counted; this makes the startup panel
  and /health flow_count accurate for multi-worker + pre-placed files.

- Fix FlowRegistry.remove() to delete both the primary store key file and
  any alternate-keyed file (UUID or stem), ensuring cross-worker DELETE
  propagation: after a delete, every worker's next request to that flow
  triggers the per-request stale check, reads None from the store, and
  returns 404 regardless of which key its cache entry uses.

- Document the per-request stale check in FlowRegistry docstring with
  latency guidance for network volumes vs. local tmpfs.

- Add tests for all of the above, including a cross-worker DELETE
  propagation scenario validated by a live 2-worker integration test.

* fix(serve): load startup flows in every uvicorn worker via LFX_SERVE_STARTUP_PATHS

Previously, lfx serve flow.json --workers 4 silently dropped startup flows:
the parent loaded them into a NullFlowStore registry that workers never
inherited. Each worker called create_serve_app() fresh and started empty,
returning flow_count: 0 from /health and 404 for the startup flow.

Fix: before calling uvicorn.run() in multi-worker mode, set
LFX_SERVE_STARTUP_PATHS to the JSON-encoded list of resolved startup file
paths (or the inline-JSON temp file path). create_serve_app() reads this env
var and re-loads the startup flows independently in each worker process using
a ThreadPoolExecutor to avoid RuntimeError from asyncio.run() being called
inside uvicorn's already-running event loop.

Startup flows loaded this way live in each worker's in-memory registry.
Uploaded flows still require --flow-dir for cross-worker sharing (existing
warning preserved). The LFX_SERVE_* env vars are cleaned up after
uvicorn.run() returns via the existing prefix-sweep finally block.

Tests added:
- test_serve_command_sets_startup_paths_env_for_multi_worker: verifies
  LFX_SERVE_STARTUP_PATHS is set before uvicorn.run() and cleaned up after
- TestCreateServeAppFactory: unit tests for create_serve_app() in all three
  modes (empty start, with startup paths, empty paths list)

* fix(serve): fix 3 worker/store consistency issues

Issue 1 — redundant store writes with --flow-dir + startup files + workers > 1:
When flow_dir is set the parent already persisted startup flows to the store.
create_serve_app() was re-loading and re-writing them in every worker.
Fix: create_serve_app() now skips LFX_SERVE_STARTUP_PATHS when flow_dir is
set and relies solely on warm_from_store(). Similarly, serve_command now only
populates LFX_SERVE_STARTUP_PATHS when flow_dir is NOT set.

Issue 2 — .py restriction was too broad:
The error blocked .py + --workers > 1 even without --flow-dir, where each
worker can safely re-execute the .py via LFX_SERVE_STARTUP_PATHS. The error
now only fires when --flow-dir is also set (workers would skip startup paths
and warm_from_store, missing the .py flow entirely since it can't be stored).

Issue 3 — parent built FastAPI app for multi-worker then discarded it:
create_multi_serve_app() was called before the workers > 1 / else branch,
building a full FastAPI app that was never served in multi-worker mode.
Moved into the else branch so it's only built for single-worker.

Tests:
- test_create_serve_app_with_flow_dir_skips_startup_paths_uses_store:
  asserts build_registry_from_paths NOT called when flow_dir is set;
  flow comes from store via warm_from_store instead.
- test_create_serve_app_without_flow_dir_loads_startup_paths_from_files:
  asserts flow IS loaded when no flow_dir.
- test_serve_command_does_not_set_startup_paths_when_flow_dir_set:
  asserts LFX_SERVE_STARTUP_PATHS is [] when flow_dir is set.
- test_serve_command_allows_py_with_multiple_workers_no_flow_dir:
  asserts .py + workers>1 + no flow_dir is allowed and path appears in env.

* test(serve): remove duplicate test and fix mislabeled test name

- Remove test_create_serve_app_loads_startup_paths: functionally identical
  to test_create_serve_app_without_flow_dir_loads_startup_paths_from_files
  (same setup, same assertions, added in a prior session).
- Rename test_verify_api_key_header_takes_precedence to
  test_verify_api_key_query_param_takes_precedence: the test passes the
  correct key as the query_param argument and the wrong key as header_param,
  then asserts the query param value is returned — query param has precedence,
  not header. The old name was the opposite of what the test actually checks.

* [autofix.ci] apply automated fixes

* fix(serve): fix concurrent request isolation and startup UX

Three bug fixes discovered via live QA:

1. Component.__deepcopy__ produced shallow copies of _outputs_map,
   _edges, and _components, causing concurrent requests to share the
   same Output objects across graph copies.  Output.cache=True by
   default: the first request's output.value was served to all
   subsequent concurrent requests, producing identical responses for
   different inputs.  Fixed by deep-copying all three fields with memo.

2. POST /flows/upload/ without an explicit id used uuid5(content),
   which is deterministic — two uploads of any flow with the same
   nodes/edges structure always generated the same UUID and the second
   always returned 409.  Fixed by using uuid4() (random).

3. Startup CLI emitted emoji and exclamation-point decoration in the
   serve banner and verbose output.  Replaced with plain text.

* [autofix.ci] apply automated fixes

* fix(serve): address PR review comments

- Move 409 conflict check before load_flow_from_json+prepare so callers
  with an explicit id don't pay the parse cost for a flow that will be
  rejected anyway
- Remove unreachable FlowAlreadyRegisteredError catch: no await exists
  between registry.get() and registry.add(), so asyncio cannot
  interleave another request in that window
- Update FlowMeta.id and UploadFlowResponse.id field descriptions from
  stale "Deterministic UUIDv5" to plain "Flow identifier (UUID)"

* test(serve): fix upload duplicate/replace tests after uuid4 change

Both tests relied on uuid5(content) determinism to generate the same
flow ID from identical data. With uuid4, each upload gets a fresh ID,
so duplicate detection was never triggered. Tests now supply an
explicit id field to exercise the 409/replace paths directly.

* fix(serve): address code review suggestions

- Fix test_write_is_atomic to glob for any *.tmp files rather than
  checking a hardcoded filename that could never exist
- Remove incorrect noqa: B904 from validate_id_is_uuid (not in an
  except block); use `from None` instead
- Add TTL cache (1 s) for FlowStore.list_ids() in FlowRegistry to
  avoid one filesystem glob per /health request; mutations invalidate
  immediately, warm_from_store and delete-scan bypass it
- Document the UUID/stem dual-key alias invariant on _flows
- Add test verifying create_serve_app() propagates load errors from
  the ThreadPoolExecutor startup path (fail fast, not silent empty registry)

* [autofix.ci] apply automated fixes

* fix: lfx serve no_env_fallback, deepcopy cycle, cross-worker delete (#13297)

fix: address concurrency and isolation issues in lfx serve

- Re-apply no_env_fallback to the per-request graph copy in the run and
  stream endpoints; deepcopy() drops graph.context so the flag was lost.
- Register the copy in memo before recursive deepcopy calls in
  Component.__deepcopy__ so reference cycles are preserved.
- Check the store directly in list_metas() so cross-worker deletes are
  reflected immediately instead of after the TTL cache window.
- Remove the redundant upload-conflict test (already covered by
  test_upload_duplicate_without_replace_returns_409).

Also mark test secrets with allowlist pragmas and fix two docstring
lint warnings in the touched test file.

* [autofix.ci] apply automated fixes

* feat(lfx): WXO/TRM request-scoped variables for lfx serve (child of #13121) (#13327)

* feat(lfx): WXO/TRM request-scoped variables for lfx serve global_vars

Align lfx serve with the TRM/WXO credential contract on top of #13121:
- Parse LANGFLOW_REQUEST_VARIABLES from global_vars payloads
- ContextVar-backed lookup in VariableService (no os.environ mutation)
- Activate request scope during execute_graph_with_capture

Child of add-workers-and-upload-lfx (PR #13121).

* [autofix.ci] apply automated fixes

* fix(lfx): harden request-scoped variable resolution + add isolation t… (#13347)

fix(lfx): harden request-scoped variable resolution + add isolation tests

Addresses PR review of WXO/TRM request-scoped variables for lfx serve:

- service: honor no_env_fallback in VariableService so model/KB credential
  resolution no longer falls back to os.environ under --no-env-fallback;
  correct the get_variable precedence docstring (5-step interleaved order)
- request_scope: add a per-request no_env_fallback ContextVar; use precise
  Token type; document the None-vs-empty scope contract
- common: activate/reset the no_env_fallback scope from graph.context
- runtime_variables: log (instead of silently swallowing) a malformed or
  non-object LANGFLOW_REQUEST_VARIABLES blob; anchor the override comment to
  the TRM contract

Tests:
- add request-scope concurrency isolation + no-env-fallback suite
- add execute_graph_with_capture wiring test (graph.context -> ContextVars)
- give TestGraphExecution mock graphs a real context, fixing 11 tests that
  broke once the helper began reading graph.context

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jordan Frazier <122494242+jordanrfrazier@users.noreply.github.com>

* Update use of stem alias tracking to ensure proper deletes

* fix(lfx): enforce no_env_fallback across all credential resolution paths

Closes credential-isolation gaps in lfx serve and hardens request-scoped
variable handling:

- VariableService: skip the process-wide LANGFLOW_REQUEST_VARIABLES env read
  when env fallback is disabled (previously leaked process credentials for an
  empty-global_vars request despite --no-env-fallback).
- Unified models: gate os.environ reads in get_api_key_for_provider and the
  connection-config fallbacks in instantiation.py (WatsonX URL/project-id,
  Ollama base-url, OpenRouter headers) on the no-env-fallback flag.
- run_until_complete: propagate the caller's contextvars into the worker
  thread so request-scoped vars and the flag survive the thread hop.
- Request variables: serialize structured (dict/list) values as valid JSON and
  drop nulls instead of producing lossy Python reprs or a truthy "None".
- FlowRegistry: extract the duplicated store-alias scan shared by add/remove.

Adds tests for request-scope isolation, credential/env gating, and contextvar
propagation.

* fix(lfx): close no_env_fallback credential leaks and harden serve isolation

- credentials: honor no_env_fallback in get_all_variables_for_provider so served
  flows no longer leak process env (was defeating the _env_if_allowed guards in
  instantiation.py via pre-filled provider_vars)
- loading: apply the no_env_fallback contract to table load_from_db column
  resolution (sibling of load_from_env_vars, previously still read os.getenv)
- variable service: resolve request-scoped vars (exact name + x-langflow-global-var
  alias) before env fallback so a caller's per-request credential is never shadowed
  by a same-named process env var
- runtime_variables: flatten the LANGFLOW_REQUEST_VARIABLES blob into graph context
  for parity between the ContextVar and load_from_db resolution paths
- flow_store: clean up the partial temp file when an atomic write fails
- commands: on multi-worker shutdown remove only the serve env vars we set, not the
  whole LFX_SERVE_* namespace
- components: add check_component_env_writes lint banning os.environ writes; stop the
  mem0 component from writing the OpenAI key into the process environment

* fix(lfx): contain corrupt flow-store files so one bad file can't crash worker startup

FilesystemFlowStore.read() only caught FileNotFoundError, so a corrupted/partial
{id}.json in a shared store (PVC corruption, an external writer killed mid-write)
raised JSONDecodeError. warm_from_store() calls get() for every store id at startup
with no per-file guard, so a single bad file would crash every worker's startup and
500 GET /flows.

- read(): catch JSONDecodeError/OSError/UnicodeDecodeError, log, return None (treat
  as absent) so warm_from_store/list_metas/get skip it and other flows keep serving.
- warm_from_store(): skip+log a flow that fails to reconstruct (corrupt JSON, or valid
  JSON referencing a component not available in this build) instead of aborting startup.
- tests: corrupt-JSON read returns None; warm_from_store skips unloadable flows.

* fix test -- request wins over env now

* fix test. mock graph context correctly

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@logspace.ai>
Co-authored-by: Himavarsha <40851462+HimavarshaVS@users.noreply.github.com>
2026-06-03 15:33:39 +00:00

494 lines
16 KiB
TOML

[project]
name = "langflow"
version = "1.10.0"
description = "A Python package with a built-in web application"
requires-python = ">=3.10,<3.15"
license = "MIT"
keywords = ["nlp", "langchain", "openai", "gpt", "gui"]
readme = "README.md"
maintainers = [
{ name = "Carlos Coelho", email = "carlos@langflow.org" },
{ name = "Cristhian Zanforlin", email = "cristhian.lousa@gmail.com" },
{ name = "Gabriel Almeida", email = "gabriel@langflow.org" },
{ name = "Lucas Eduoli", email = "lucaseduoli@gmail.com" },
{ name = "Otávio Anovazzi", email = "otavio2204@gmail.com" },
{ name = "Rodrigo Nader", email = "rodrigo@langflow.org" },
{ name = "Italo dos Anjos", email = "italojohnnydosanjos@gmail.com" },
]
# Define your main dependencies here
dependencies = [
"langflow-base[complete]>=0.10.0",
# langflow-extensions:bundle-deps-start
"lfx-duckduckgo>=0.1.0",
"lfx-arxiv>=0.1.0",
"lfx-ibm>=0.1.0",
"lfx-docling>=0.1.0",
# langflow-extensions:bundle-deps-end
]
[dependency-groups]
dev = [
"pytest-instafail>=0.5.0",
"ipykernel>=6.29.0",
"ruff~=0.13.1",
"httpx>=0.28.1",
"pytest>=9.0.3",
"requests>=2.33.0",
"pytest-cov>=5.0.0",
"pytest-mock>=3.14.0",
"pytest-xdist>=3.6.0",
"pytest-sugar>=1.0.0",
"respx>=0.21.1",
"pytest-asyncio>=0.23.0",
"pytest-profiling>=1.7.0",
"pre-commit>=3.7.0",
"vulture>=2.11",
"dictdiffer>=0.9.0",
"pytest-split>=0.9.0",
"pytest-flakefinder>=1.1.0",
"packaging>=24.1,<25.0",
"asgi-lifespan>=2.1.0",
"pytest-github-actions-annotate-failures>=0.2.0",
"blockbuster>=1.5.20,<1.6",
"types-aiofiles>=24.1.0.20240626",
"codeflash>=0.8.4",
"hypothesis>=6.123.17",
"locust~=2.43.4",
"pytest-rerunfailures>=15.0",
"scrapegraph-py>=1.10.2",
'elevenlabs==1.58.1; python_version == "3.12"',
'elevenlabs>=1.52.0; python_version != "3.12"',
"faker>=37.0.0",
"pytest-timeout>=2.3.1",
"pyyaml>=6.0.2",
"pyleak>=0.1.14",
"mcp-server-fetch>=2025.1.17",
"onnxruntime>=1.20,<1.24; python_version<'3.14'", # >=1.24 does not support Python 3.10; <1.24 allows 1.23.x for agent-lifecycle-toolkit
"onnxruntime>=1.26; python_version>='3.14'",
"fakeredis>=2.0.0",
]
[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true
[tool.uv.sources]
langflow-base = { workspace = true }
langflow = { workspace = true }
lfx = { workspace = true }
langflow-sdk = { workspace = true }
# langflow-extensions:bundle-sources-start
lfx-duckduckgo = { workspace = true }
lfx-arxiv = { workspace = true }
lfx-ibm = { workspace = true }
lfx-docling = { workspace = true }
# langflow-extensions:bundle-sources-end
torch = { index = "pytorch-cpu" }
torchvision = { index = "pytorch-cpu" }
[tool.uv.workspace]
members = [
"src/backend/base",
".",
"src/lfx",
"src/langflow-stepflow",
"src/sdk",
# langflow-extensions:bundle-members-start
"src/bundles/duckduckgo",
"src/bundles/arxiv",
"src/bundles/ibm",
"src/bundles/docling",
# langflow-extensions:bundle-members-end
]
[tool.hatch.build.targets.wheel]
packages = ["src/backend/langflow"]
[project.urls]
Repository = "https://github.com/langflow-ai/langflow"
Documentation = "https://docs.langflow.org"
[project.optional-dependencies]
docling = [
"lfx-docling[local]>=0.1.0",
]
docling-chunking = [
"lfx-docling[chunking]>=0.1.0",
]
docling-image-description = [
"lfx-docling[image-description]>=0.1.0",
]
audio = [
"webrtcvad>=2.0.10",
]
couchbase = [
"couchbase>=4.2.1"
]
cassio = [
"cassio>=0.1.7"
]
local = [
"sentence-transformers>=2.3.1",
"ctransformers>=0.2.10"
]
clickhouse-connect = [
"clickhouse-connect==0.7.19"
]
nv-ingest = [
"nv-ingest-api>=26.1.0,<27.0.0 ; python_version >= '3.12'",
"nv-ingest-client>=26.1.0,<27.0.0 ; python_version >= '3.12'",
]
postgresql = [
"sqlalchemy[postgresql_psycopg2binary]>=2.0.38,<3.0.0",
"sqlalchemy[postgresql_psycopg]>=2.0.38,<3.0.0",
]
[tool.uv]
override-dependencies = [
# temporary force a newer python-pptx
"python-pptx>=1.0.2",
# z3-solver 4.15.7 dropped Linux wheels, pin until codeflash is removed
"z3-solver<4.15.7",
# Security: Force upgraded versions to prevent transitive deps from pulling older vulnerable versions
"orjson>=3.11.6",
"gunicorn>=25.3.0",
"pypdf>=6.10.0", # Force pypdf 6.10+ to prevent vulnerable 6.6.x versions
"nltk>=3.9.4",
"Markdown>=3.8.0",
"dynaconf>=3.2.13",
"pillow>=12.1.1", # Force Pillow 12.1.1+ to prevent CVE-vulnerable versions
"playwright>=1.59.0", # Latest available on PyPI; ensures updated Chromium with CVE fixes
# Transitive dependency CVE fixes
"lxml>=6.1.0,<7.0.0", # CVE-2026-41066
"mako>=1.3.12,<2.0.0", # CVE-2026-44307
"urllib3>=2.7.0,<3.0.0", # CVE-2026-44431, CVE-2026-44432
"python-liquid>=2.2.0,<3.0.0", # CVE-2026-45017
]
[project.scripts]
langflow = "langflow.langflow_launcher:main"
[tool.codespell]
skip = '.git,*.pdf,*.svg,*.pdf,*.yaml,*.ipynb,poetry.lock,*.min.js,*.css,package-lock.json,*.trig.,**/node_modules/**,./stuff/*,*.csv'
# Ignore latin etc
ignore-regex = '.*(Stati Uniti|Tense=Pres).*'
[tool.pytest.ini_options]
timeout = 150
timeout_method = "signal"
minversion = "6.0"
testpaths = ["src/backend/tests", "src/lfx/tests", "src/bundles/*/tests"]
console_output_style = "progress"
filterwarnings = [
"ignore::DeprecationWarning",
"ignore::ResourceWarning",
"ignore:Skipped unsupported reflection:sqlalchemy.exc.SAWarning",
"ignore:.*SQL-parsed foreign key constraint:sqlalchemy.exc.SAWarning",
"ignore:autogenerate skipping metadata-specified expression-based index:UserWarning",
]
log_cli = true
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
markers = [
"async_test",
"api_key_required",
"no_blockbuster",
"benchmark",
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow-running tests",
"security: Security regression tests (IDOR, auth, access control)"
]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
addopts = "-p no:benchmark"
[tool.coverage.run]
command_line = """
-m pytest --ignore=tests/integration
--cov --cov-report=term --cov-report=html
--instafail -ra -n auto -m "not api_key_required"
"""
source = ["src/backend/base/langflow/"]
omit = ["*/alembic/*", "tests/*", "*/__init__.py"]
[tool.coverage.report]
sort = "Stmts"
skip_empty = true
show_missing = false
ignore_errors = true
[tool.coverage.html]
directory = "coverage"
[tool.ruff]
target-version = "py310"
exclude = [
"src/backend/base/langflow/alembic/*",
"src/frontend/tests/assets/*",
"src/lfx/src/lfx/_assets/component_index.json",
"docs/**/API-Reference/python-examples/**",
]
line-length = 120
[tool.ruff.lint]
flake8-annotations.mypy-init-return = true
flake8-bugbear.extend-immutable-calls = [
"fastapi.Depends",
"fastapi.File",
"fastapi.Query",
"typer.Argument",
"typer.Option",
]
flake8-type-checking.runtime-evaluated-base-classes = [
"pydantic.BaseModel",
"typing.TypedDict", # Needed by fastapi
"typing_extensions.TypedDict", # Needed by fastapi
]
pydocstyle.convention = "google"
select = ["ALL"]
ignore = [
"C90", # McCabe complexity
"CPY", # Missing copyright
"COM812", # Messes with the formatter
"ERA", # Eradicate commented-out code
"FIX002", # Line contains TODO
"ISC001", # Messes with the formatter
"PERF203", # Rarely useful
"PLR09", # Too many something (arg, statements, etc)
"RUF012", # Pydantic models are currently not well detected. See https://github.com/astral-sh/ruff/issues/13630
"TD002", # Missing author in TODO
"TD003", # Missing issue link in TODO
"TRY301", # A bit too harsh (Abstract `raise` to an inner function)
"PLC0415", # Inline imports
"D10", # Missing docstrings
"PLW1641", # Object does not implement `__hash__` method (mutable objects shouldn't be hashable)
# Rules that are TODOs
"ANN"
]
# Preview rules that are not yet activated
external = ["RUF027"]
[tool.ruff.lint.per-file-ignores]
"scripts/*" = ["D1", "INP", "T201"]
"scripts/gp/tests/*" = [
"D1", # Missing docstrings
"PLR2004", # Magic value comparisons
"S101", # Use of assert (required for pytest)
"TRY003", # Long exception messages
"EM101", # String literals in exceptions
]
"scripts/ci/test_*.py" = [
"D1", # Missing docstrings
"PLR2004", # Magic value comparisons
"S101", # Use of assert (required for pytest)
"TRY003", # Long exception messages
"EM101", # String literals in exceptions
]
"src/backend/base/langflow/alembic/versions/*" = ["INP001", "D415", "PGH003"]
"src/backend/base/langflow/custom/__init__.py" = [
"I001", # Import order affects initialization - must import custom module first
]
"src/backend/base/langflow/api/v1/*" = [
"TCH", # FastAPI needs to evaluate types at runtime
]
"src/backend/base/langflow/api/v2/*" = [
"TCH", # FastAPI needs to evaluate types at runtime
]
"src/backend/base/langflow/__main__.py" = [
"B008", # Typer CLI requires function calls in defaults
]
"src/backend/base/langflow/services/cache/*" = [
"S301", # Pickle usage is intentional for caching
]
"src/backend/base/langflow/services/tracing/*" = [
"SLF001", # Third-party library private member access (langwatch, opik)
]
"src/lfx/src/lfx/base/curl/parse.py" = [
"S105", # False positive: 'token' variable name, not a password
]
"src/lfx/src/lfx/services/auth/service.py" = [
"ARG002", # No-op impl: unused args required by interface
"EM101", # NotImplementedError messages as literals
"TC003", # Type-only imports used in signatures
]
"src/lfx/src/lfx/base/mcp/util.py" = [
"SLF001", # MCP library private member access
]
"src/lfx/src/lfx/cli/common.py" = [
"S104", # Intentional binding to all interfaces for server
"S105", # False positive: GITHUB_TOKEN_ENV is an env var name
]
"src/lfx/src/lfx/components/__init__.py" = [
"SLF001", # Accessing _dynamic_imports from modules
]
"src/lfx/src/lfx/components/data/save_file.py" = [
"SLF001", # Google API client private member access
]
"src/lfx/src/lfx/components/datastax/astradb_vectorstore.py" = [
"S110", # Try-except-pass for optional metadata
]
"src/lfx/src/lfx/components/google/google_generative_ai_embeddings.py" = [
"SLF001", # Google AI library private member access
]
"src/lfx/src/lfx/components/knowledge_bases/retrieval.py" = [
"SLF001", # Chroma client private member access
]
"src/lfx/src/lfx/components/mongodb/mongodb_atlas.py" = [
"SLF001", # MongoDB collection private member access
]
"src/lfx/src/lfx/components/tools/{python_code_structured_tool.py,searxng.py}" = [
"S102", # Use of exec/eval for dynamic code execution
"SLF001", # Component internal member access
]
"src/lfx/src/lfx/components/vectorstores/astradb.py" = [
"S110", # Try-except-pass for optional metadata
]
"src/lfx/src/lfx/custom/{code_parser/code_parser.py,validate.py}" = [
"S102", # Use of exec for code validation
]
"src/lfx/src/lfx/custom/custom_component/component.py" = [
"SLF001", # Component internal state management
]
"src/lfx/src/lfx/custom/directory_reader/directory_reader.py" = [
"SLF001", # Component introspection
]
"src/lfx/src/lfx/custom/utils.py" = [
"SLF001", # Component code analysis
]
"src/lfx/src/lfx/graph/graph/ascii.py" = [
"SLF001", # Grandalf library private member access
]
"src/lfx/src/lfx/inputs/input_mixin.py" = [
"S105", # False positive: PASSWORD is a type constant
]
"src/lfx/src/lfx/__main__.py" = [
"B008", # Typer CLI requires function calls in argument defaults
"FBT001", # Bool flags are the standard typer pattern
"FBT003", # Boolean positional values in typer.Option() calls
]
"src/lfx/src/lfx/cli/_setup_commands.py" = [
"B008", # Typer CLI requires function calls in argument defaults
"FBT001", # Bool flags are the standard typer pattern
"FBT003", # Boolean positional values in typer.Option() calls
]
"src/lfx/src/lfx/cli/_authoring_commands.py" = [
"B008", # Typer CLI requires function calls in argument defaults
"FBT001", # Bool flags are the standard typer pattern
"FBT003", # Boolean positional values in typer.Option() calls
]
"src/lfx/src/lfx/cli/_running_commands.py" = [
"B008", # Typer CLI requires function calls in argument defaults
"FBT001", # Bool flags are the standard typer pattern
"FBT003", # Boolean positional values in typer.Option() calls
]
"src/lfx/src/lfx/cli/_remote_commands.py" = [
"B008", # Typer CLI requires function calls in argument defaults
"FBT001", # Bool flags are the standard typer pattern
"FBT003", # Boolean positional values in typer.Option() calls
]
"src/backend/base/langflow/api/utils/*" = [
"TCH", # Imports are used at runtime (FastAPI deps, SQLAlchemy queries, model constructors)
]
"src/sdk/src/langflow_sdk/_http.py" = [
"TCH", # httpx is used at runtime (response object methods)
]
"src/sdk/src/langflow_sdk/environments.py" = [
"TRY003", # Contextual error messages are necessary here
"EM102", # f-string messages assigned before raise where possible
]
"src/sdk/tests/*" = [
"S101", # assert is the standard pytest assertion style
"INP001", # Not a package namespace issue in tests
"TC003", # pytest fixture type hints are evaluated at runtime
]
"src/lfx/src/lfx/schema/table.py" = [
"S105", # False positive: PASSWORD is a formatter type
]
"src/lfx/src/lfx/services/mcp_composer/service.py" = [
"S104", # Intentional binding to all interfaces for server
"S110", # Try-except-pass for optional error logging
]
"src/backend/tests/*" = [
"D1",
"PLR2004",
"S101",
"SLF001",
"BLE001", # allow broad-exception catching in tests
]
"src/backend/base/langflow/tests/*" = [
"D1",
"PLR2004",
"S101",
"SLF001",
"BLE001", # allow broad-exception catching in tests
]
"src/lfx/tests/*" = [
"D1",
"PLR2004",
"S101",
"SLF001",
"BLE001", # allow broad-exception catching in tests
"S104", # Binding to all interfaces (test servers)
"S108", # Insecure temp file usage (safe in tests)
]
"src/bundles/*/tests/*" = [
"D1",
"PLR2004",
"S101",
"SLF001",
"BLE001", # allow broad-exception catching in tests
"S104", # Binding to all interfaces (test servers)
"S108", # Insecure temp file usage (safe in tests)
"INP001", # Bundle test dirs are not import packages
]
"src/backend/tests/locust/*" = [
"D1", # Missing docstrings (CLI tools don't need full docstrings)
"T201", # Print statements (needed for CLI output)
"S603", # Subprocess calls (needed for running commands)
"S607", # Starting process with partial executable path
"S104", # Binding to all interfaces (needed for Langflow server)
"S105", # Hardcoded passwords (test credentials)
"FBT001", # Boolean-typed positional arguments (common in CLI)
"FBT002", # Boolean default arguments (common in CLI tools)
"TRY002", # Custom exceptions (generic exceptions OK for CLI)
"TRY003", # Long exception messages (descriptive errors for users)
"TRY300", # Consider moving to else block (return patterns)
"EM101", # String literals in exceptions (user-facing messages)
"EM102", # F-string literals in exceptions (user-facing messages)
"EXE001", # Shebang without executable (may be run via python)
"D415", # First line punctuation (CLI docstrings)
"F401", # Unused imports (imports for availability checking)
"PTH123", # Use Path.open (open() is fine for simple cases)
"PTH107", # Use Path.unlink (os.remove is fine for simple cases)
"PTH207", # Use Path.glob (glob.glob is fine for simple cases)
"B007", # Unused loop variables (tuple unpacking)
"PLW0602", # Global variable usage (needed for locust state)
"PLW0603", # Global variable updates (needed for locust state)
"DTZ005", # Datetime without timezone (local time is fine for logs)
"G004", # Logging f-strings (detailed error logging)
"SIM102", # Nested if statements (readability over optimization)
"E501", # Line too long (some CLI commands are naturally long)
]
[tool.ruff.lint.flake8-builtins]
builtins-allowed-modules = [ "io", "logging", "socket"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"