lfx 1.14.0.dev2__py3-none-any.whl → 1.14.0.dev4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5469,7 +5469,7 @@
5469
5469
  "icon": "folder",
5470
5470
  "legacy": true,
5471
5471
  "metadata": {
5472
- "code_hash": "ba7aa7c4c975",
5472
+ "code_hash": "f34e352b2cbe",
5473
5473
  "component_class_name": "DirectoryComponent",
5474
5474
  "dependencies": {
5475
5475
  "dependencies": [
@@ -5523,7 +5523,7 @@
5523
5523
  "show": true,
5524
5524
  "title_case": false,
5525
5525
  "type": "code",
5526
- "value": "from pathlib import Path, PurePath, PureWindowsPath\n\nfrom lfx.base.data.utils import TEXT_FILE_TYPES, parallel_load_data, parse_text_file_to_data, retrieve_file_paths\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, IntInput, MessageTextInput, MultiselectInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.services.deps import get_settings_service\nfrom lfx.template.field.base import Output\nfrom lfx.utils.file_path_security import component_file_access_scopes, enforce_local_file_access\n\n\nclass DirectoryComponent(Component):\n display_name = \"Directory\"\n description = \"Recursively load files from a directory.\"\n documentation: str = \"https://docs.langflow.org/directory\"\n icon = \"folder\"\n name = \"Directory\"\n legacy = True\n replacement = [\"files_and_knowledge.File\"]\n\n inputs = [\n MessageTextInput(\n name=\"path\",\n display_name=\"Path\",\n info=\"Path to the directory to load files from. Defaults to current directory ('.')\",\n value=\".\",\n tool_mode=True,\n ),\n MultiselectInput(\n name=\"types\",\n display_name=\"File Types\",\n info=\"File types to load. Select one or more types or leave empty to load all supported types.\",\n options=TEXT_FILE_TYPES,\n value=[],\n ),\n IntInput(\n name=\"depth\",\n display_name=\"Depth\",\n info=\"Depth to search for files.\",\n value=0,\n ),\n IntInput(\n name=\"max_concurrency\",\n display_name=\"Max Concurrency\",\n advanced=True,\n info=\"Maximum concurrency for loading files.\",\n value=2,\n ),\n BoolInput(\n name=\"load_hidden\",\n display_name=\"Load Hidden\",\n advanced=True,\n info=\"If true, hidden files will be loaded.\",\n ),\n BoolInput(\n name=\"recursive\",\n display_name=\"Recursive\",\n advanced=True,\n info=\"If true, the search will be recursive.\",\n ),\n BoolInput(\n name=\"silent_errors\",\n display_name=\"Silent Errors\",\n advanced=True,\n info=\"If true, errors will not raise an exception.\",\n ),\n BoolInput(\n name=\"use_multithreading\",\n display_name=\"Use Multithreading\",\n advanced=True,\n info=\"If true, multithreading will be used.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Loaded Files\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n @staticmethod\n def _has_parent_reference(path: str) -> bool:\n path_parts = (*PurePath(path).parts, *PureWindowsPath(path).parts)\n return any(part == \"..\" for part in path_parts)\n\n def _allowed_roots(self) -> list[Path]:\n roots = {Path.cwd().resolve()}\n configured_roots = getattr(get_settings_service().settings, \"directory_component_allowed_roots\", []) or []\n for root in configured_roots:\n if root:\n roots.add(Path(root).expanduser().resolve())\n return list(roots)\n\n def _resolve_directory_path(self, path: str) -> str:\n path = str(path or \".\").strip()\n # Reject null bytes and parent references outright. A drive-absolute or UNC\n # path (e.g. ``D:\\\\shared\\\\docs``) is NOT blanket-rejected here: canonicalization\n # plus the ``_allowed_roots()`` containment check below decides whether it is\n # allowed, so operator-configured roots on another drive remain reachable.\n if \"\\x00\" in path or self._has_parent_reference(path):\n msg = \"Directory path escapes the allowed root.\"\n raise ValueError(msg)\n\n resolved_path = Path(self.resolve_path(path)).expanduser().resolve()\n if not any(resolved_path == root or resolved_path.is_relative_to(root) for root in self._allowed_roots()):\n msg = \"Directory path escapes the allowed root.\"\n raise ValueError(msg)\n return str(resolved_path)\n\n def load_directory(self) -> list[Data]:\n path = self.path\n types = self.types\n depth = self.depth\n max_concurrency = self.max_concurrency\n load_hidden = self.load_hidden\n recursive = self.recursive\n silent_errors = self.silent_errors\n use_multithreading = self.use_multithreading\n\n resolved_path = self._resolve_directory_path(path)\n\n # Security: confine directory reads to the storage dir in restricted (multi-tenant)\n # mode so a tenant cannot recursively read arbitrary server directories.\n resolved_path = str(enforce_local_file_access(resolved_path, scope_ids=component_file_access_scopes(self)))\n\n # If no types are specified, use all supported types\n if not types:\n types = TEXT_FILE_TYPES\n\n # Check if all specified types are valid\n invalid_types = [t for t in types if t not in TEXT_FILE_TYPES]\n if invalid_types:\n msg = f\"Invalid file types specified: {invalid_types}. Valid types are: {TEXT_FILE_TYPES}\"\n raise ValueError(msg)\n\n valid_types = types\n\n file_paths = retrieve_file_paths(\n resolved_path, load_hidden=load_hidden, recursive=recursive, depth=depth, types=valid_types\n )\n\n loaded_data = []\n if use_multithreading:\n loaded_data = parallel_load_data(file_paths, silent_errors=silent_errors, max_concurrency=max_concurrency)\n else:\n loaded_data = [parse_text_file_to_data(file_path, silent_errors=silent_errors) for file_path in file_paths]\n\n valid_data = [x for x in loaded_data if x is not None and isinstance(x, Data)]\n self.status = valid_data\n return valid_data\n\n def as_dataframe(self) -> DataFrame:\n return DataFrame(self.load_directory())\n"
5526
+ "value": "from pathlib import Path, PurePath, PureWindowsPath\n\nimport lfx\nfrom lfx.base.data.utils import TEXT_FILE_TYPES, parallel_load_data, parse_text_file_to_data, retrieve_file_paths\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, IntInput, MessageTextInput, MultiselectInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.services.deps import get_settings_service\nfrom lfx.template.field.base import Output\nfrom lfx.utils.file_path_security import component_file_access_scopes, enforce_local_file_access\n\n\nclass DirectoryComponent(Component):\n display_name = \"Directory\"\n description = \"Recursively load files from a directory.\"\n documentation: str = \"https://docs.langflow.org/directory\"\n icon = \"folder\"\n name = \"Directory\"\n legacy = True\n replacement = [\"files_and_knowledge.File\"]\n\n inputs = [\n MessageTextInput(\n name=\"path\",\n display_name=\"Path\",\n info=\"Path to the directory to load files from. Defaults to current directory ('.')\",\n value=\".\",\n tool_mode=True,\n ),\n MultiselectInput(\n name=\"types\",\n display_name=\"File Types\",\n info=\"File types to load. Select one or more types or leave empty to load all supported types.\",\n options=TEXT_FILE_TYPES,\n value=[],\n ),\n IntInput(\n name=\"depth\",\n display_name=\"Depth\",\n info=\"Depth to search for files.\",\n value=0,\n ),\n IntInput(\n name=\"max_concurrency\",\n display_name=\"Max Concurrency\",\n advanced=True,\n info=\"Maximum concurrency for loading files.\",\n value=2,\n ),\n BoolInput(\n name=\"load_hidden\",\n display_name=\"Load Hidden\",\n advanced=True,\n info=\"If true, hidden files will be loaded.\",\n ),\n BoolInput(\n name=\"recursive\",\n display_name=\"Recursive\",\n advanced=True,\n info=\"If true, the search will be recursive.\",\n ),\n BoolInput(\n name=\"silent_errors\",\n display_name=\"Silent Errors\",\n advanced=True,\n info=\"If true, errors will not raise an exception.\",\n ),\n BoolInput(\n name=\"use_multithreading\",\n display_name=\"Use Multithreading\",\n advanced=True,\n info=\"If true, multithreading will be used.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Loaded Files\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n @staticmethod\n def _has_parent_reference(path: str) -> bool:\n path_parts = (*PurePath(path).parts, *PureWindowsPath(path).parts)\n return any(part == \"..\" for part in path_parts)\n\n @staticmethod\n def _bundled_components_root() -> Path:\n # The component sources shipped with lfx are public, read-only package files that the\n # Langflow Assistant reads wherever lfx is installed, whatever the working directory.\n return (Path(lfx.__file__).parent / \"components\").resolve()\n\n def _allowed_roots(self) -> list[Path]:\n roots = {Path.cwd().resolve(), self._bundled_components_root()}\n configured_roots = getattr(get_settings_service().settings, \"directory_component_allowed_roots\", []) or []\n for root in configured_roots:\n if root:\n roots.add(Path(root).expanduser().resolve())\n return list(roots)\n\n def _resolve_directory_path(self, path: str) -> str:\n path = str(path or \".\").strip()\n # Reject null bytes and parent references outright. A drive-absolute or UNC\n # path (e.g. ``D:\\\\shared\\\\docs``) is NOT blanket-rejected here: canonicalization\n # plus the ``_allowed_roots()`` containment check below decides whether it is\n # allowed, so operator-configured roots on another drive remain reachable.\n if \"\\x00\" in path or self._has_parent_reference(path):\n msg = \"Directory path escapes the allowed root.\"\n raise ValueError(msg)\n\n resolved_path = Path(self.resolve_path(path)).expanduser().resolve()\n if not any(resolved_path == root or resolved_path.is_relative_to(root) for root in self._allowed_roots()):\n msg = \"Directory path escapes the allowed root.\"\n raise ValueError(msg)\n return str(resolved_path)\n\n def load_directory(self) -> list[Data]:\n path = self.path\n types = self.types\n depth = self.depth\n max_concurrency = self.max_concurrency\n load_hidden = self.load_hidden\n recursive = self.recursive\n silent_errors = self.silent_errors\n use_multithreading = self.use_multithreading\n\n resolved_path = self._resolve_directory_path(path)\n\n # Security: confine directory reads to the storage dir in restricted (multi-tenant)\n # mode so a tenant cannot recursively read arbitrary server directories.\n if not Path(resolved_path).is_relative_to(self._bundled_components_root()):\n resolved_path = str(enforce_local_file_access(resolved_path, scope_ids=component_file_access_scopes(self)))\n\n # If no types are specified, use all supported types\n if not types:\n types = TEXT_FILE_TYPES\n\n # Check if all specified types are valid\n invalid_types = [t for t in types if t not in TEXT_FILE_TYPES]\n if invalid_types:\n msg = f\"Invalid file types specified: {invalid_types}. Valid types are: {TEXT_FILE_TYPES}\"\n raise ValueError(msg)\n\n valid_types = types\n\n file_paths = retrieve_file_paths(\n resolved_path, load_hidden=load_hidden, recursive=recursive, depth=depth, types=valid_types\n )\n\n loaded_data = []\n if use_multithreading:\n loaded_data = parallel_load_data(file_paths, silent_errors=silent_errors, max_concurrency=max_concurrency)\n else:\n loaded_data = [parse_text_file_to_data(file_path, silent_errors=silent_errors) for file_path in file_paths]\n\n valid_data = [x for x in loaded_data if x is not None and isinstance(x, Data)]\n self.status = valid_data\n return valid_data\n\n def as_dataframe(self) -> DataFrame:\n return DataFrame(self.load_directory())\n"
5527
5527
  },
5528
5528
  "depth": {
5529
5529
  "_input_type": "IntInput",
@@ -32518,6 +32518,6 @@
32518
32518
  "num_components": 130,
32519
32519
  "num_modules": 17
32520
32520
  },
32521
- "sha256": "efaa0601bd07c0b923e42faff8a4c27579aacfd0ca3c689b3e9ad978c1e17ab4",
32522
- "version": "1.14.0.dev2"
32521
+ "sha256": "d6908e8a66b45d5c246e965258c469a724f6b114a7de55827005e0b6f8fab4e2",
32522
+ "version": "1.14.0.dev4"
32523
32523
  }
@@ -58,10 +58,21 @@ REMEDIATIONS: tuple[Remediation, ...] = (
58
58
  providers=("OpenAI",),
59
59
  ),
60
60
  Remediation(
61
- # Claude 5 rejects a temperature its 4.x siblings accept (GH-14291); markers
62
- # stay narrow so an out-of-range value, which only the user can fix, surfaces.
61
+ # Models that only accept the default temperature reject an explicitly set
62
+ # one at call time: Claude 5 calls it "deprecated" (GH-14291), while OpenAI
63
+ # and Azure reasoning models (o-series, gpt-5*) answer with "'temperature'
64
+ # is not supported with this model" or "'temperature' does not support
65
+ # <value> ... Only the default (1) value is supported". Dropping temperature
66
+ # (→ provider default) is the fix for all of them. Markers stay narrow so a
67
+ # genuine out-of-range value ("temperature must be between 0 and 2"), which
68
+ # only the user can fix, still surfaces.
63
69
  name="temperature-unsupported",
64
- markers=("`temperature` is deprecated", "temperature is deprecated"),
70
+ markers=(
71
+ "`temperature` is deprecated",
72
+ "temperature is deprecated",
73
+ "'temperature' is not supported",
74
+ "'temperature' does not support",
75
+ ),
65
76
  overrides={"temperature": None},
66
77
  ),
67
78
  )
@@ -1,5 +1,6 @@
1
1
  from pathlib import Path, PurePath, PureWindowsPath
2
2
 
3
+ import lfx
3
4
  from lfx.base.data.utils import TEXT_FILE_TYPES, parallel_load_data, parse_text_file_to_data, retrieve_file_paths
4
5
  from lfx.custom.custom_component.component import Component
5
6
  from lfx.io import BoolInput, IntInput, MessageTextInput, MultiselectInput
@@ -82,8 +83,14 @@ class DirectoryComponent(Component):
82
83
  path_parts = (*PurePath(path).parts, *PureWindowsPath(path).parts)
83
84
  return any(part == ".." for part in path_parts)
84
85
 
86
+ @staticmethod
87
+ def _bundled_components_root() -> Path:
88
+ # The component sources shipped with lfx are public, read-only package files that the
89
+ # Langflow Assistant reads wherever lfx is installed, whatever the working directory.
90
+ return (Path(lfx.__file__).parent / "components").resolve()
91
+
85
92
  def _allowed_roots(self) -> list[Path]:
86
- roots = {Path.cwd().resolve()}
93
+ roots = {Path.cwd().resolve(), self._bundled_components_root()}
87
94
  configured_roots = getattr(get_settings_service().settings, "directory_component_allowed_roots", []) or []
88
95
  for root in configured_roots:
89
96
  if root:
@@ -120,7 +127,8 @@ class DirectoryComponent(Component):
120
127
 
121
128
  # Security: confine directory reads to the storage dir in restricted (multi-tenant)
122
129
  # mode so a tenant cannot recursively read arbitrary server directories.
123
- resolved_path = str(enforce_local_file_access(resolved_path, scope_ids=component_file_access_scopes(self)))
130
+ if not Path(resolved_path).is_relative_to(self._bundled_components_root()):
131
+ resolved_path = str(enforce_local_file_access(resolved_path, scope_ids=component_file_access_scopes(self)))
124
132
 
125
133
  # If no types are specified, use all supported types
126
134
  if not types:
@@ -13,11 +13,11 @@ before constructing the graph. The rewrite is intentionally tolerant:
13
13
  * Unknown shape (non-dict node, missing 'data' field) -> skipped, no
14
14
  error. The caller's own validation handles malformed payloads.
15
15
 
16
- The rewriter mutates ``payload`` in place because the saved-flow object is
17
- already a freshly-decoded dict tree owned by the caller; copying every node
18
- just to swap one string per node is wasteful and would force callers to
19
- re-bind their reference. This is the one mutation in the migration system;
20
- everything else is frozen Pydantic models.
16
+ The rewriter mutates ``payload`` in place. ``Graph.from_payload`` passes the
17
+ normalized copy it builds from, so the caller's saved flow is never rewritten.
18
+ Copying every node again just to swap one string per node would add a second
19
+ copy and force callers to re-bind their reference. This is the one mutation
20
+ in the migration system, and everything else is frozen Pydantic models.
21
21
 
22
22
  The function returns a :class:`MigrationReport` with one record per node it
23
23
  visited so the caller (today: nothing; tomorrow: ``ExtensionEventsService``)
lfx/graph/graph/base.py CHANGED
@@ -52,6 +52,7 @@ from lfx.services.authorization.base import ExecutionPrincipal
52
52
  from lfx.services.cache.utils import CacheMiss
53
53
  from lfx.services.deps import get_chat_service, get_tracing_service
54
54
  from lfx.utils.async_helpers import run_until_complete
55
+ from lfx.utils.flow_code import normalize_code_for_import
55
56
 
56
57
  try:
57
58
  from opentelemetry import trace as otel_trace
@@ -1848,6 +1849,7 @@ class Graph:
1848
1849
  validate_flow_for_current_settings,
1849
1850
  )
1850
1851
 
1852
+ payload = normalize_code_for_import(payload)
1851
1853
  if "data" in payload:
1852
1854
  payload = payload["data"]
1853
1855
  # Catalog identity is the exact, case-sensitive stored node.data.type.
lfx/load/load.py CHANGED
@@ -15,6 +15,7 @@ from lfx.load.utils import replace_tweaks_with_env
15
15
  from lfx.log.logger import configure
16
16
  from lfx.processing.process import process_tweaks, run_graph
17
17
  from lfx.utils.async_helpers import run_until_complete
18
+ from lfx.utils.flow_code import normalize_code_for_import
18
19
  from lfx.utils.flow_validation import CustomComponentValidationError, ensure_component_hash_lookups_loaded
19
20
  from lfx.utils.util import update_settings
20
21
 
@@ -76,7 +77,7 @@ async def aload_flow_from_json(
76
77
  msg = "Input must be either a file path (str) or a JSON object (dict)"
77
78
  raise TypeError(msg)
78
79
 
79
- graph_data = flow_graph["data"]
80
+ graph_data = normalize_code_for_import(flow_graph)["data"]
80
81
  if tweaks is not None:
81
82
  graph_data = process_tweaks(graph_data, tweaks)
82
83
 
lfx/schema/workflow.py CHANGED
@@ -511,26 +511,20 @@ class WorkflowResumeResponse(BaseModel):
511
511
  message: str | None = None
512
512
 
513
513
 
514
+ # FastAPI registers ``model`` in components.schemas and emits oneOf $refs.
515
+ # Inlining model_json_schema() leaves the discriminator mapping pointing at names
516
+ # that are never created, which Redoc cannot resolve.
517
+ WorkflowExecutionResult = Annotated[
518
+ WorkflowExecutionResponse | WorkflowJobResponse,
519
+ Field(discriminator="object"),
520
+ ]
521
+
514
522
  # OpenAPI response definitions
515
523
  WORKFLOW_EXECUTION_RESPONSES = {
516
524
  200: {
525
+ "model": WorkflowExecutionResult,
517
526
  "description": "Workflow execution response",
518
527
  "content": {
519
- "application/json": {
520
- "schema": {
521
- "oneOf": [
522
- WorkflowExecutionResponse.model_json_schema(),
523
- WorkflowJobResponse.model_json_schema(),
524
- ],
525
- "discriminator": {
526
- "propertyName": "object",
527
- "mapping": {
528
- "response": "#/components/schemas/WorkflowExecutionResponse",
529
- "job": "#/components/schemas/WorkflowJobResponse",
530
- },
531
- },
532
- }
533
- },
534
528
  "text/event-stream": {
535
529
  "schema": WorkflowStreamEvent.model_json_schema(),
536
530
  "description": "Server-sent events for streaming execution",
@@ -250,6 +250,8 @@ class RuntimeSettings(BaseModel):
250
250
  no inbound provider deliveries at all (a firewalled install using Track B
251
251
  listeners only); every ingress request then answers 404 exactly as an
252
252
  unknown trigger id does, so disabling it leaks nothing either."""
253
+ trigger_ingress_origin: str | None = None
254
+ """Operator-advertised HTTPS origin for push providers without OAuth callbacks."""
253
255
  trigger_ingress_max_body_bytes: int = Field(default=1_048_576, gt=0)
254
256
  """Per-route body cap for an ingress delivery, independent of the global
255
257
  request limit. Provider notifications are small - Graph basic notifications
lfx/upgrade/checker.py CHANGED
@@ -6,6 +6,8 @@ from collections.abc import Mapping
6
6
  from dataclasses import dataclass, field
7
7
  from typing import Any, Literal
8
8
 
9
+ from lfx.utils.flow_code import normalize_code_for_import
10
+
9
11
  COMPONENTS_TO_IGNORE_UPDATE: frozenset[str] = frozenset({"CustomComponent"})
10
12
  TRANSIENT_TEMPLATE_KEYS: frozenset[str] = frozenset({"is_refresh", "tools_metadata"})
11
13
 
@@ -278,6 +280,7 @@ def check_flow_compatibility(
278
280
  rebuilding the lookup when the caller also runs ``apply_safe_upgrades`` on the same
279
281
  registry.
280
282
  """
283
+ flow_data = normalize_code_for_import(flow_data)
281
284
  if registry is None:
282
285
  registry = build_registry_lookup(all_types_dict)
283
286
  statuses: list[NodeStatus] = []
lfx/utils/flow_code.py ADDED
@@ -0,0 +1,46 @@
1
+ """Normalize source code in exported flow payloads."""
2
+
3
+ from collections.abc import Iterator
4
+ from copy import deepcopy
5
+
6
+ from lfx.utils.flow_envelope import split_flow_envelope
7
+
8
+
9
+ def _code_fields(flow: dict) -> Iterator[dict]:
10
+ _, graph = split_flow_envelope(flow)
11
+ nodes = graph.get("nodes")
12
+ if not isinstance(nodes, list):
13
+ return
14
+ for node in nodes:
15
+ if not isinstance(node, dict):
16
+ continue
17
+ node_data = node.get("data")
18
+ if not isinstance(node_data, dict):
19
+ continue
20
+ component = node_data.get("node")
21
+ if not isinstance(component, dict):
22
+ continue
23
+ template = component.get("template")
24
+ if not isinstance(template, dict):
25
+ continue
26
+ for field_data in template.values():
27
+ if isinstance(field_data, dict) and field_data.get("type") == "code":
28
+ yield field_data
29
+
30
+
31
+ def has_code_lines(flow: dict) -> bool:
32
+ """Return whether any code field still stores exported code as a list of lines."""
33
+ return any(isinstance(field_data.get("value"), list) for field_data in _code_fields(flow))
34
+
35
+
36
+ def normalize_code_for_import(flow: dict) -> dict:
37
+ """Return a copy with exported code lines joined without losing trailing newlines."""
38
+ normalized = deepcopy(flow)
39
+ for field_data in _code_fields(normalized):
40
+ value = field_data.get("value")
41
+ if isinstance(value, list):
42
+ if not all(isinstance(line, str) for line in value):
43
+ msg = "Code lines must all be strings"
44
+ raise ValueError(msg)
45
+ field_data["value"] = "\n".join(value)
46
+ return normalized
@@ -42,6 +42,8 @@ from functools import lru_cache
42
42
  from pathlib import Path
43
43
  from types import MappingProxyType
44
44
 
45
+ from lfx.utils.flow_code import normalize_code_for_import
46
+
45
47
  # ---------------------------------------------------------------------------
46
48
  # Standard-library module names (3.10+)
47
49
  # ---------------------------------------------------------------------------
@@ -548,7 +550,7 @@ def generate_requirements_from_flow(
548
550
  all_packages: set[str] = set()
549
551
  all_providers: set[str] = set()
550
552
 
551
- data = flow.get("data", {})
553
+ data = normalize_code_for_import(flow).get("data", {})
552
554
  nodes = data.get("nodes", [])
553
555
 
554
556
  for node in nodes:
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any
9
9
 
10
10
  from lfx.log.logger import logger
11
11
  from lfx.utils.component_aliases import ComponentIdentityIndex, get_component_type_aliases
12
+ from lfx.utils.flow_code import has_code_lines, normalize_code_for_import
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from lfx.services.catalog_policy import CatalogPolicySnapshot
@@ -276,11 +277,24 @@ def _extract_graph_payload(graph: Any) -> Mapping[str, Any] | None:
276
277
 
277
278
 
278
279
  def _extract_flow_data(target: Mapping[str, Any] | Any | None) -> dict[str, Any] | None:
279
- """Normalize a flow payload or graph-like object to raw graph data."""
280
+ """Normalize a flow payload or graph-like object to raw graph data.
281
+
282
+ Downloaded flows store component code as lists of lines, and ``Graph.from_payload`` joins
283
+ them before it builds. Join them here as well, on a copy, so policy checks hash and sanitize
284
+ the source that will run. Lines that cannot be joined stay as they are, and the code checks
285
+ still block them.
286
+ """
280
287
  if isinstance(target, Mapping) or target is None:
281
- return _normalize_flow_data(target)
288
+ flow_data = _normalize_flow_data(target)
289
+ else:
290
+ flow_data = _normalize_flow_data(_extract_graph_payload(target))
282
291
 
283
- return _normalize_flow_data(_extract_graph_payload(target))
292
+ if flow_data is None or not has_code_lines(flow_data):
293
+ return flow_data
294
+ try:
295
+ return normalize_code_for_import(flow_data)
296
+ except ValueError:
297
+ return flow_data
284
298
 
285
299
 
286
300
  def _collect_catalog_component_keys(nodes: Any) -> set[str]:
@@ -1317,10 +1331,10 @@ def substitute_outdated_component_code_in_place(
1317
1331
  claim is likewise left untouched, so drift is never resolved into the *wrong* component —
1318
1332
  see :class:`SubstitutableComponentTypes`.
1319
1333
 
1320
- Mutates ``flow_data`` in place, matching ``Graph.from_payload``'s existing contract with
1321
- ``migrate_flow_payload``; callers that must preserve the stored flow (so the editor keeps
1322
- flagging the node as outdated) pass a copy. Returns ``display_name (id)`` labels for the
1323
- nodes that were swapped.
1334
+ Mutates ``flow_data`` in place. ``Graph.from_payload`` passes the normalized copy it builds
1335
+ from, so a stored flow is never changed there. Other callers that must preserve the stored
1336
+ flow, so the editor keeps flagging the node as outdated, pass a copy. Returns
1337
+ ``display_name (id)`` labels for the nodes that were swapped.
1324
1338
 
1325
1339
  When ``validate_public_execution`` is true, the exact post-substitution bytes are checked
1326
1340
  against the public code-execution policy using the same registry snapshot that supplied
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lfx
3
- Version: 1.14.0.dev2
3
+ Version: 1.14.0.dev4
4
4
  Summary: Langflow Executor - A lightweight CLI tool for executing and serving Langflow AI flows
5
5
  Author-email: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
6
6
  Requires-Python: <3.16,>=3.10
@@ -18,7 +18,8 @@ Requires-Dist: defusedxml<1.0.0,>=0.7.1
18
18
  Requires-Dist: docstring-parser<1.0.0,>=0.16
19
19
  Requires-Dist: emoji<3.0.0,>=2.14.1
20
20
  Requires-Dist: fastapi<1.0.0,>=0.135.0
21
- Requires-Dist: filelock<4.0.0,>=3.20.1
21
+ Requires-Dist: filelock<4.2.0,>=4.1.0; python_version < '3.11'
22
+ Requires-Dist: filelock<5.0.0,>=4.2.1; python_version >= '3.11'
22
23
  Requires-Dist: gunicorn>=22.0; sys_platform != 'win32'
23
24
  Requires-Dist: h2>=4.4.1
24
25
  Requires-Dist: httpx[http2]<1.0.0,>=0.24.0
@@ -27,7 +28,7 @@ Requires-Dist: jsonschema<5.0.0,>=4.20.0
27
28
  Requires-Dist: langchain-classic~=1.0.7
28
29
  Requires-Dist: langchain-core<2.0.0,>=1.2.28
29
30
  Requires-Dist: langchain~=1.3.0
30
- Requires-Dist: langflow-sdk==0.4.0.dev96
31
+ Requires-Dist: langflow-sdk==0.4.0.dev98
31
32
  Requires-Dist: loguru<1.0.0,>=0.7.3
32
33
  Requires-Dist: markitdown<2.0.0,>=0.1.4; python_version < '3.15'
33
34
  Requires-Dist: mcp<2.0.0,>=1.30.0
@@ -77,7 +78,7 @@ Provides-Extra: sqlite
77
78
  Requires-Dist: apsw==3.53.4.0; extra == 'sqlite'
78
79
  Requires-Dist: sqlite-vec==0.1.9; extra == 'sqlite'
79
80
  Provides-Extra: toolguard
80
- Requires-Dist: lfx-toolguard<1.0.0,>=0.1.0; extra == 'toolguard'
81
+ Requires-Dist: lfx-toolguard<1.0.0,>=0.3.0; extra == 'toolguard'
81
82
  Description-Content-Type: text/markdown
82
83
 
83
84
  # LFX - Langflow Executor
@@ -10,7 +10,7 @@ lfx/preload.py,sha256=5YciPjPCRAZhiYKQYsLKcNO-phS9vBdo7VF46_khfms,13737
10
10
  lfx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  lfx/settings.py,sha256=wnx4zkOLQ8mvampYsnnvVV9GvEnRUuWQpKFSbFTCIp4,181
12
12
  lfx/type_extraction.py,sha256=3gPIl9AMA8UquNG7E_Oh4_9nk5BGlUVuB97qjSuu5zg,2792
13
- lfx/_assets/component_index.json,sha256=YpTorqs3UaVLD-_5O-5DZaURIOWYPzCaRUdkwC9STdQ,2475661
13
+ lfx/_assets/component_index.json,sha256=wxZAs85Loiu3Af6lq1dA2AQ1XI7eWJGnisWeYwnpe6c,2476122
14
14
  lfx/_assets/legacy_storage_sources.json,sha256=3OWkuQ30fWqBeuctyYOQUBZqEreC5kJhS9B6NfSGm8I,183917
15
15
  lfx/_assets/stable_hash_history.json,sha256=WZjooLc3zQgc1kUWF5aoWtYyOuTHT-qmh7ZtiFcEsUg,41694
16
16
  lfx/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -127,7 +127,7 @@ lfx/base/models/llm_callback_utils.py,sha256=0GLzdlUDxLOfF-oQsUykUDB1iaVpA2zOiAT
127
127
  lfx/base/models/model.py,sha256=WuQkDrq9RY7yU2SziiRj0UjIxTO46_ZXq8yXFNCeuo4,21896
128
128
  lfx/base/models/model_input_constants.py,sha256=5XUR9OLmukyYWGXem9o5suFYvhJNAWiEdNUPoR6COxk,12882
129
129
  lfx/base/models/model_metadata.py,sha256=OHuwAqav-ay77rtlfYqbzJ2ZIq--3fGZW3CJxQh3oFI,18934
130
- lfx/base/models/model_remediation.py,sha256=qcE-3L6Q3T_6nOLV72c62ZDo2s4bKDoXy6f5-_BJJqs,5997
130
+ lfx/base/models/model_remediation.py,sha256=ONHZ_E35ZNS4n6ncb1yK4_fuT4f4zRKjdA91k7rkU_U,6585
131
131
  lfx/base/models/model_utils.py,sha256=-WXD2t9oBVk-t0oNNOon6GjHLSMpZE_8Zy1ShGVwbtA,58743
132
132
  lfx/base/models/models_dev_catalog.py,sha256=7OIOxizp_jcxh4INJnBD8uyOZDE7KGS_29EoD019b88,21839
133
133
  lfx/base/models/novita_constants.py,sha256=_mgBYGwpddUw4CLhLKJl-psOUzA_SQGHrfZJUNes6aI,1247
@@ -301,7 +301,7 @@ lfx/components/files_and_knowledge/__init__.py,sha256=uqO6FlF-J_cbv0w53e-6rc4nFI
301
301
  lfx/components/files_and_knowledge/_filesystem_isolation.py,sha256=dBSJSZUW0DAqtc3nbdFz2ElTuSWWXDQSk02x-eQNtlc,1669
302
302
  lfx/components/files_and_knowledge/_filesystem_namespace.py,sha256=9HOFQIZ75jqzIQ-73yapsMsGkilgRD6L-Ur2t2cLhlM,3022
303
303
  lfx/components/files_and_knowledge/_kb_paths.py,sha256=zUEQYM4kTexATtj1tZpdbGLwOmHJDbl1d_-G0jFNTPI,3719
304
- lfx/components/files_and_knowledge/directory.py,sha256=unqnxMl1iaJNCl6ZBm9H3izRdvJJghL-wNbWkVdD-f0,5954
304
+ lfx/components/files_and_knowledge/directory.py,sha256=8041Kyy-EeDSdKA8YTuJcz_Xscte2OUUBolrn14SgFg,6405
305
305
  lfx/components/files_and_knowledge/file.py,sha256=GWu8GmX4aqjt_cDeH9f23QKt4oLUCmsKK4NZCQW_4ws,70528
306
306
  lfx/components/files_and_knowledge/filesystem.py,sha256=jwRPBZoe92ZZyGLOzg5h063uR0fOYH-OszsDBuQvGqQ,56709
307
307
  lfx/components/files_and_knowledge/ingestion.py,sha256=dabhpFeXDDMkgk9IdlUSriV4sncJwpyKsK4yaIrYiJU,1954
@@ -565,7 +565,7 @@ lfx/extension/loader/_types.py,sha256=FH6pEgb5zpIVXIIQg0XDPpVWDGWzb1vil0YXEzhYzR
565
565
  lfx/extension/migration/__init__.py,sha256=CwRSy0t3Bz2usgrKuuSGEr9iRBInoFFiQEJaiZOhY6U,1754
566
566
  lfx/extension/migration/loader.py,sha256=enDNoi7JMEmffGL4HmydYW40govunBMC2R3hcJsLlho,6081
567
567
  lfx/extension/migration/migration_table.json,sha256=V29p8LeSHX-YXZCJlnT2LHrZ-py3r3Q94DSHyMeR2f0,188056
568
- lfx/extension/migration/rewrite.py,sha256=lq8-bG05me0h3wUdKWmc-aRsVUuQ-J6i0KkMIDMnQtE,19872
568
+ lfx/extension/migration/rewrite.py,sha256=O9T_Ggzk3tzGX4PgQ18fmwUkzGDaD1jvcP_2m3Ui8CM,19913
569
569
  lfx/extension/migration/schema.py,sha256=meM64U7p5IQHE2VIfsvLoO7Ue0AwvH716Sb4NQkvEkQ,16717
570
570
  lfx/field_typing/__init__.py,sha256=1SIlVWBB9WTTLUNSmiR0LdMNY2pGg_-jnCF4Ll6FlG4,1808
571
571
  lfx/field_typing/conditional_options.py,sha256=RmJXXv_Bxq_aavF9t9Daa7OsWrprZXL-BAAVJgizyzo,770
@@ -595,7 +595,7 @@ lfx/graph/flow_builder/layout.py,sha256=ZCdVrmJ-Fv2LMBq35Dd2LPx1sbncr9M0zGncTgyx
595
595
  lfx/graph/flow_builder/spec.py,sha256=9f8L3mlOtHgazqu1KR3vvQml0UjQleRdApzV-PIij3M,8264
596
596
  lfx/graph/graph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
597
597
  lfx/graph/graph/ascii.py,sha256=-jYWI_Zmz24mfLOxJrzIINZ0dbQgd9PXJfqwZa03xas,6211
598
- lfx/graph/graph/base.py,sha256=VjOcQua2VCvHUTXsCsZMPH2ryBJtrCzCm7t4Q4_rxvw,152454
598
+ lfx/graph/graph/base.py,sha256=oP1pQHi5KdCl0gGGtENkE0getc1EXwLPE35Zui-JJOU,152565
599
599
  lfx/graph/graph/constants.py,sha256=jwjl4RydV_k_zawbI8FIgiLHeBBgH-cStVitxxSyXQs,1641
600
600
  lfx/graph/graph/runnable_vertices_manager.py,sha256=c-qQP3koKyAsIADDSONiiz4FIRIn6q5kAMX6EQIBBfA,6148
601
601
  lfx/graph/graph/schema.py,sha256=1a8pictdRVN6ByqpvhOYu7heDeVs7A3eGpZuYIF7kaY,1086
@@ -638,7 +638,7 @@ lfx/interface/initialize/loading.py,sha256=mRB7_N4xnS2e0oe5qiQ00SaDo0g-nzkC9CDmC
638
638
  lfx/io/__init__.py,sha256=Yihh5qVCVYJRDG3AjVpX2IQqEy-6FS7zbgHqlgKbjy4,1337
639
639
  lfx/io/schema.py,sha256=L6hE9SAJqbx9BH9wQU8zAjDSfa9XCni8C2S-oTyAgsw,15396
640
640
  lfx/load/__init__.py,sha256=y35GBUhVTOsG3GzL5UVL-RNAsu0D7T8MVPrNXoDMx7U,224
641
- lfx/load/load.py,sha256=hJSV_1_yjFasFM2BGOa9M8_i0jov5YPlnpLwgFksaqM,10924
641
+ lfx/load/load.py,sha256=KGjG1oTECRZGe1kTwhxTMfmatf3ep_8QJtiO1fY4yzA,11009
642
642
  lfx/load/utils.py,sha256=4mJ3_sAt8DdBVIiOHV8aGJchglF2IYTbLAQfwUkcN8k,4543
643
643
  lfx/log/__init__.py,sha256=gJLNG2h6ptId2U9EyXHYmTlYNHESsSibmHhd3hPetRU,197
644
644
  lfx/log/_streams.py,sha256=UAX_39RvJslajjE9HwadPuUnbfjMUnglOKjVJlFFauw,1195
@@ -691,7 +691,7 @@ lfx/schema/serialize.py,sha256=Y7aL91w3BW4ZYkgdIHosUYdpIJUDku-SoqYoIQtwtGM,252
691
691
  lfx/schema/table.py,sha256=1RdMmk1vbzmPFW0JEGhP7WDDRtvUKH9lkDw5v-JWhSw,4668
692
692
  lfx/schema/token_usage.py,sha256=6FU_v0xc5Gd-AA7lv67Yvx2mEzN6GCRXLH8ik06VbJo,8166
693
693
  lfx/schema/validators.py,sha256=1SwbWr44uoIOsoWb1BxqvHq104SFh_jhLJVOKGTw4sE,4769
694
- lfx/schema/workflow.py,sha256=KyAkLFgKuxjQxDetIyRoVsU_MUBAI0Ec-FU59KeDjsE,21251
694
+ lfx/schema/workflow.py,sha256=C2aVEx9fztyyD1ycIef1bb_NQGK-qvqvQXmZoEpiRKU,21002
695
695
  lfx/serialization/__init__.py,sha256=g-CG7Y--Eog6mSPMI7wqXh6J8lYygFthqmJWrCa3P7k,145
696
696
  lfx/serialization/constants.py,sha256=mAYNbapEPu9-KixSHVPCXUyM-bsFMjSoCc4ohkQGvQY,47
697
697
  lfx/serialization/serialization.py,sha256=NdcX7KXeP69tF4R1ABB2w4IFRKxrUFotyZVg04k78aU,12532
@@ -779,7 +779,7 @@ lfx/services/settings/groups/database.py,sha256=pkMJYWzeZjWTPkIRLvBz5Cf0m9sBhSnR
779
779
  lfx/services/settings/groups/mcp.py,sha256=hL0Id8I-w6NEtlCQxYuqUBkkukaJECvzxS4ooRX3tFo,10434
780
780
  lfx/services/settings/groups/observability.py,sha256=Q09KEmP0FUeD6gMacIspcbcNa8VTYQJafCxe1PqFTPs,2486
781
781
  lfx/services/settings/groups/paths.py,sha256=IwDpt28fCXyUPrS1HJs6ebELaAcaqLBMP04JtL7doWI,3796
782
- lfx/services/settings/groups/runtime.py,sha256=dsURg2kji3vYmjZGsyaLrMVMpJt_GHNAZFxt7mSEnDU,28055
782
+ lfx/services/settings/groups/runtime.py,sha256=JxVFU3pmt8fEH1Cg_eQQxMzAhmeG1zizFSqjH5BP21I,28188
783
783
  lfx/services/settings/groups/security.py,sha256=Gu4yAdjmSyro0m5BX4WZAOwnmR8V8gTCMRcPpWEBkB8,28786
784
784
  lfx/services/settings/groups/server.py,sha256=k0qcRAD-mK11MDpjLDvL27-i2ltZafPPcb0MAkzwBVg,5281
785
785
  lfx/services/settings/groups/storage.py,sha256=uZgVWRFkI2wxZ5g3uoWyNKB2fclrGJ64OZ9bMrISWVM,628
@@ -837,7 +837,7 @@ lfx/type_extraction/__init__.py,sha256=8kKUjudbOHkYHVEOA14jD4A-9UUlG1lj1rznKuWoP
837
837
  lfx/type_extraction/type_extraction.py,sha256=B4P-eAfUD9i0NqohdntgU3NBLU_0jDc18L36lQsYhrY,2718
838
838
  lfx/upgrade/__init__.py,sha256=f4yVhS7B-nHW_tvUi3hzr4ZDSEwqRQRukCH_uniecDE,277
839
839
  lfx/upgrade/applier.py,sha256=xkbA-digcmmpdwVd1hYz0D7RLNmQIs6V53QWX1y_Z0M,3871
840
- lfx/upgrade/checker.py,sha256=jBg14O1egZgNeu6Ge8YuJUfOIVIN6dkYKsdyhB1B49g,13435
840
+ lfx/upgrade/checker.py,sha256=4K-QRaj9W-0aMPoWKstsiZBxWHmA2IJ0eviY0tw3zWw,13547
841
841
  lfx/upgrade/cli_gate.py,sha256=SRfRleATX6klHzCTUu0kiF38OmEgof8UkBfDDo5sK1Y,4878
842
842
  lfx/upgrade/registry.py,sha256=rqOl9G3ucamM4bxSZY4WHR10Ivt6T-mDPNaQAhSvJKU,1975
843
843
  lfx/utils/__init__.py,sha256=X5mPJ2NuCxF0hobaFDBq4IGqpZFonPaIokeoFSHuX2E,881
@@ -852,9 +852,10 @@ lfx/utils/end_user_storage.py,sha256=M83ec39bI54yI14IW5dA-laysrLTcdxLhQyAXEjFSlE
852
852
  lfx/utils/env_var_security.py,sha256=cngMWWB-Wl3U1ypxHd_pvS_uHaNyKXgzOB3hcrOcEwA,4365
853
853
  lfx/utils/exceptions.py,sha256=xLkPFy5CgMiw8QyZc5UXgUjmBNY9MdJ4XXzaYAVcavQ,3190
854
854
  lfx/utils/file_path_security.py,sha256=8V-WYsQRFlgGvToeoUdCj1K-5-w_AMORaQY4LUlD23c,17616
855
+ lfx/utils/flow_code.py,sha256=4R7UlXKHApJ4zPUIJt9qRAWOl2hWMlX5jK2MGKnX4ss,1664
855
856
  lfx/utils/flow_envelope.py,sha256=D0eCjaOyvQ5gWSqdsoL7hQwNKf_6fJTToC9yZNHNq8w,2302
856
- lfx/utils/flow_requirements.py,sha256=w7DbqD4oa4txnmYqpo8cIzAgS0-tXLy_qBpUywW3t2o,28671
857
- lfx/utils/flow_validation.py,sha256=ZhPRYxu6VoYdNKzYo5sA1brsj0raUBgrdCaS-5OeHuQ,104622
857
+ lfx/utils/flow_requirements.py,sha256=AsioxS7ZlN7tB7H6Pbylt-eXByclVjS7MbMiN3AJXAU,28757
858
+ lfx/utils/flow_validation.py,sha256=pHPygVg3-fer3HKgyarHiye5cPAUEW5BolIvtCBego0,105258
858
859
  lfx/utils/helpers.py,sha256=0LE0barnVp-8Y5cCoDRzhDzesvXqgiT7IXP6vtTSyGE,889
859
860
  lfx/utils/image.py,sha256=CaMGwpCCKxcu_pWmP7FZXwZ3kr1-ymGKjN92Bv_yr5k,3160
860
861
  lfx/utils/jq_security.py,sha256=mADEtPPbQvoQw88JOXB1Ig9Eh5uslijYw8U_ZPho7_U,8571
@@ -889,7 +890,7 @@ lfx/workflow/router.py,sha256=lrIDbrrE0XwI2fbfxpt8jb2jPSyxTpthUqUY1HXVVCo,24620
889
890
  lfx/workflow/adapters/__init__.py,sha256=8WyVggf_LacLJRJEUM6_4JLqM2EN-KogrbUEp1E8w00,5166
890
891
  lfx/workflow/adapters/agui.py,sha256=ZLLJJ-CQgB1ce8HhqjQt4AtR3a70MSweC5F5StUgW0c,3479
891
892
  lfx/workflow/adapters/langflow.py,sha256=0RT1uZWBz7O1JmOlfiQVruvXBIX8FPhXwGdLQchvJ5I,8845
892
- lfx-1.14.0.dev2.dist-info/METADATA,sha256=Y1YvsS_zBbtNgbk6Box-H02VzYNhS_S9rrjY_P3r-1g,35297
893
- lfx-1.14.0.dev2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
894
- lfx-1.14.0.dev2.dist-info/entry_points.txt,sha256=jKPaACW4536e5puzF8Po_sPx7_GvktOBvttxOwJiMMs,104
895
- lfx-1.14.0.dev2.dist-info/RECORD,,
893
+ lfx-1.14.0.dev4.dist-info/METADATA,sha256=xbk4nzHr3j80nfYOVWkWrFXUthpg-rXzPDNw28ZDgFY,35385
894
+ lfx-1.14.0.dev4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
895
+ lfx-1.14.0.dev4.dist-info/entry_points.txt,sha256=jKPaACW4536e5puzF8Po_sPx7_GvktOBvttxOwJiMMs,104
896
+ lfx-1.14.0.dev4.dist-info/RECORD,,