lfx-nightly 0.2.0.dev41__py3-none-any.whl → 0.3.0.dev3__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.
Files changed (98) hide show
  1. lfx/__main__.py +137 -6
  2. lfx/_assets/component_index.json +1 -1
  3. lfx/base/agents/agent.py +10 -6
  4. lfx/base/agents/altk_base_agent.py +5 -3
  5. lfx/base/agents/altk_tool_wrappers.py +1 -1
  6. lfx/base/agents/events.py +1 -1
  7. lfx/base/agents/utils.py +4 -0
  8. lfx/base/composio/composio_base.py +78 -41
  9. lfx/base/data/cloud_storage_utils.py +156 -0
  10. lfx/base/data/docling_utils.py +130 -55
  11. lfx/base/datastax/astradb_base.py +75 -64
  12. lfx/base/embeddings/embeddings_class.py +113 -0
  13. lfx/base/models/__init__.py +11 -1
  14. lfx/base/models/google_generative_ai_constants.py +33 -9
  15. lfx/base/models/model_metadata.py +6 -0
  16. lfx/base/models/ollama_constants.py +196 -30
  17. lfx/base/models/openai_constants.py +37 -10
  18. lfx/base/models/unified_models.py +1123 -0
  19. lfx/base/models/watsonx_constants.py +43 -4
  20. lfx/base/prompts/api_utils.py +40 -5
  21. lfx/base/tools/component_tool.py +2 -9
  22. lfx/cli/__init__.py +10 -2
  23. lfx/cli/commands.py +3 -0
  24. lfx/cli/run.py +65 -409
  25. lfx/cli/script_loader.py +18 -7
  26. lfx/cli/validation.py +6 -3
  27. lfx/components/__init__.py +0 -3
  28. lfx/components/composio/github_composio.py +1 -1
  29. lfx/components/cuga/cuga_agent.py +39 -27
  30. lfx/components/data_source/api_request.py +4 -2
  31. lfx/components/datastax/astradb_assistant_manager.py +4 -2
  32. lfx/components/docling/__init__.py +45 -11
  33. lfx/components/docling/docling_inline.py +39 -49
  34. lfx/components/docling/docling_remote.py +1 -0
  35. lfx/components/elastic/opensearch_multimodal.py +1733 -0
  36. lfx/components/files_and_knowledge/file.py +384 -36
  37. lfx/components/files_and_knowledge/ingestion.py +8 -0
  38. lfx/components/files_and_knowledge/retrieval.py +10 -0
  39. lfx/components/files_and_knowledge/save_file.py +91 -88
  40. lfx/components/langchain_utilities/ibm_granite_handler.py +211 -0
  41. lfx/components/langchain_utilities/tool_calling.py +37 -6
  42. lfx/components/llm_operations/batch_run.py +64 -18
  43. lfx/components/llm_operations/lambda_filter.py +213 -101
  44. lfx/components/llm_operations/llm_conditional_router.py +39 -7
  45. lfx/components/llm_operations/structured_output.py +38 -12
  46. lfx/components/models/__init__.py +16 -74
  47. lfx/components/models_and_agents/agent.py +51 -203
  48. lfx/components/models_and_agents/embedding_model.py +171 -255
  49. lfx/components/models_and_agents/language_model.py +54 -318
  50. lfx/components/models_and_agents/mcp_component.py +96 -10
  51. lfx/components/models_and_agents/prompt.py +105 -18
  52. lfx/components/ollama/ollama_embeddings.py +111 -29
  53. lfx/components/openai/openai_chat_model.py +1 -1
  54. lfx/components/processing/text_operations.py +580 -0
  55. lfx/components/vllm/__init__.py +37 -0
  56. lfx/components/vllm/vllm.py +141 -0
  57. lfx/components/vllm/vllm_embeddings.py +110 -0
  58. lfx/custom/custom_component/component.py +65 -10
  59. lfx/custom/custom_component/custom_component.py +8 -6
  60. lfx/events/observability/__init__.py +0 -0
  61. lfx/events/observability/lifecycle_events.py +111 -0
  62. lfx/field_typing/__init__.py +57 -58
  63. lfx/graph/graph/base.py +40 -1
  64. lfx/graph/utils.py +109 -30
  65. lfx/graph/vertex/base.py +75 -23
  66. lfx/graph/vertex/vertex_types.py +0 -5
  67. lfx/inputs/__init__.py +2 -0
  68. lfx/inputs/input_mixin.py +55 -0
  69. lfx/inputs/inputs.py +120 -0
  70. lfx/interface/components.py +24 -7
  71. lfx/interface/initialize/loading.py +42 -12
  72. lfx/io/__init__.py +2 -0
  73. lfx/run/__init__.py +5 -0
  74. lfx/run/base.py +464 -0
  75. lfx/schema/__init__.py +50 -0
  76. lfx/schema/data.py +1 -1
  77. lfx/schema/image.py +26 -7
  78. lfx/schema/message.py +104 -11
  79. lfx/schema/workflow.py +171 -0
  80. lfx/services/deps.py +12 -0
  81. lfx/services/interfaces.py +43 -1
  82. lfx/services/mcp_composer/service.py +7 -1
  83. lfx/services/schema.py +1 -0
  84. lfx/services/settings/auth.py +95 -4
  85. lfx/services/settings/base.py +11 -1
  86. lfx/services/settings/constants.py +2 -0
  87. lfx/services/settings/utils.py +82 -0
  88. lfx/services/storage/local.py +13 -8
  89. lfx/services/transaction/__init__.py +5 -0
  90. lfx/services/transaction/service.py +35 -0
  91. lfx/tests/unit/components/__init__.py +0 -0
  92. lfx/utils/constants.py +2 -0
  93. lfx/utils/mustache_security.py +79 -0
  94. lfx/utils/validate_cloud.py +81 -3
  95. {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/METADATA +7 -2
  96. {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/RECORD +98 -80
  97. {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/WHEEL +0 -0
  98. {lfx_nightly-0.2.0.dev41.dist-info → lfx_nightly-0.3.0.dev3.dist-info}/entry_points.txt +0 -0
@@ -1,9 +1,74 @@
1
1
  import platform
2
2
  from pathlib import Path
3
3
 
4
+ from cryptography.hazmat.primitives import serialization
5
+ from cryptography.hazmat.primitives.asymmetric import rsa
6
+ from cryptography.hazmat.primitives.serialization import load_pem_private_key
7
+
4
8
  from lfx.log.logger import logger
5
9
 
6
10
 
11
+ class RSAKeyError(Exception):
12
+ """Exception raised when RSA key operations fail."""
13
+
14
+
15
+ def derive_public_key_from_private(private_key_pem: str) -> str:
16
+ """Derive a public key from a private key PEM string.
17
+
18
+ Args:
19
+ private_key_pem: The private key in PEM format.
20
+
21
+ Returns:
22
+ str: The public key in PEM format.
23
+
24
+ Raises:
25
+ RSAKeyError: If the private key is invalid or cannot be processed.
26
+ """
27
+ try:
28
+ private_key = load_pem_private_key(private_key_pem.encode(), password=None)
29
+ return (
30
+ private_key.public_key()
31
+ .public_bytes(
32
+ encoding=serialization.Encoding.PEM,
33
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
34
+ )
35
+ .decode("utf-8")
36
+ )
37
+ except Exception as e:
38
+ msg = f"Failed to derive public key from private key: {e}"
39
+ logger.error(msg)
40
+ raise RSAKeyError(msg) from e
41
+
42
+
43
+ def generate_rsa_key_pair() -> tuple[str, str]:
44
+ """Generate an RSA key pair for RS256 JWT signing.
45
+
46
+ Returns:
47
+ tuple[str, str]: A tuple of (private_key_pem, public_key_pem) as strings.
48
+ """
49
+ private_key = rsa.generate_private_key(
50
+ public_exponent=65537,
51
+ key_size=2048,
52
+ )
53
+
54
+ private_key_pem = private_key.private_bytes(
55
+ encoding=serialization.Encoding.PEM,
56
+ format=serialization.PrivateFormat.PKCS8,
57
+ encryption_algorithm=serialization.NoEncryption(),
58
+ ).decode("utf-8")
59
+
60
+ public_key_pem = (
61
+ private_key.public_key()
62
+ .public_bytes(
63
+ encoding=serialization.Encoding.PEM,
64
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
65
+ )
66
+ .decode("utf-8")
67
+ )
68
+
69
+ return private_key_pem, public_key_pem
70
+
71
+
7
72
  def set_secure_permissions(file_path: Path) -> None:
8
73
  if platform.system() in {"Linux", "Darwin"}: # Unix/Linux/Mac
9
74
  file_path.chmod(0o600)
@@ -38,3 +103,20 @@ def write_secret_to_file(path: Path, value: str) -> None:
38
103
 
39
104
  def read_secret_from_file(path: Path) -> str:
40
105
  return path.read_text(encoding="utf-8")
106
+
107
+
108
+ def write_public_key_to_file(path: Path, value: str) -> None:
109
+ """Write a public key to file with appropriate permissions (0o644).
110
+
111
+ Public keys can be readable by others but should only be writable by owner.
112
+
113
+ Args:
114
+ path: The file path to write to.
115
+ value: The public key content.
116
+ """
117
+ path.write_text(value, encoding="utf-8")
118
+ try:
119
+ if platform.system() in {"Linux", "Darwin"}:
120
+ path.chmod(0o644)
121
+ except Exception: # noqa: BLE001
122
+ logger.exception("Failed to set permissions on public key file")
@@ -58,11 +58,12 @@ class LocalStorageService(StorageService):
58
58
  return str(self.data_dir / flow_id / file_name)
59
59
 
60
60
  def parse_file_path(self, full_path: str) -> tuple[str, str]:
61
- """Parse a full local storage path to extract flow_id and file_name.
61
+ r"""Parse a full local storage path to extract flow_id and file_name.
62
62
 
63
63
  Args:
64
64
  full_path: Filesystem path, may or may not include data_dir
65
- e.g., "/data/user_123/image.png" or "user_123/image.png"
65
+ e.g., "/data/user_123/image.png" or "user_123/image.png". On Windows the
66
+ separators may be backslashes ("\\"). This method handles both.
66
67
 
67
68
  Returns:
68
69
  tuple[str, str]: A tuple of (flow_id, file_name)
@@ -78,15 +79,19 @@ class LocalStorageService(StorageService):
78
79
  # Remove data_dir if present (but don't require it)
79
80
  path_without_prefix = full_path
80
81
  if full_path.startswith(data_dir_str):
81
- path_without_prefix = full_path[len(data_dir_str) :].lstrip("/")
82
+ # Strip both POSIX and Windows separators
83
+ path_without_prefix = full_path[len(data_dir_str) :].lstrip("/").lstrip("\\")
82
84
 
83
- # Split from the right to get the filename
84
- # Everything before the last "/" is the flow_id
85
- if "/" not in path_without_prefix:
86
- return "", path_without_prefix
85
+ # Normalize separators so downstream logic is platform-agnostic
86
+ normalized_path = path_without_prefix.replace("\\", "/")
87
+
88
+ # Split from the right to get the filename; everything before the last
89
+ # "/" is the flow_id
90
+ if "/" not in normalized_path:
91
+ return "", normalized_path
87
92
 
88
93
  # Use rsplit to split from the right, limiting to 1 split
89
- flow_id, file_name = path_without_prefix.rsplit("/", 1)
94
+ flow_id, file_name = normalized_path.rsplit("/", 1)
90
95
  return flow_id, file_name
91
96
 
92
97
  async def save_file(self, flow_id: str, file_name: str, data: bytes, *, append: bool = False) -> None:
@@ -0,0 +1,5 @@
1
+ """Transaction service module for lfx."""
2
+
3
+ from lfx.services.transaction.service import NoopTransactionService
4
+
5
+ __all__ = ["NoopTransactionService"]
@@ -0,0 +1,35 @@
1
+ """Transaction service implementations for lfx."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from lfx.services.interfaces import TransactionServiceProtocol
8
+
9
+
10
+ class NoopTransactionService(TransactionServiceProtocol):
11
+ """No-operation transaction service for standalone lfx mode.
12
+
13
+ This service is used when lfx runs without a concrete transaction
14
+ service implementation (e.g., without langflow). All operations
15
+ are no-ops and transaction logging is disabled.
16
+ """
17
+
18
+ async def log_transaction(
19
+ self,
20
+ flow_id: str,
21
+ vertex_id: str,
22
+ inputs: dict[str, Any] | None,
23
+ outputs: dict[str, Any] | None,
24
+ status: str,
25
+ target_id: str | None = None,
26
+ error: str | None = None,
27
+ ) -> None:
28
+ """No-op implementation of transaction logging.
29
+
30
+ In standalone mode, transactions are not persisted.
31
+ """
32
+
33
+ def is_enabled(self) -> bool:
34
+ """Transaction logging is disabled in noop mode."""
35
+ return False
File without changes
lfx/utils/constants.py CHANGED
@@ -71,6 +71,7 @@ DIRECT_TYPES = [
71
71
  "float",
72
72
  "Any",
73
73
  "prompt",
74
+ "mustache",
74
75
  "code",
75
76
  "NestedDict",
76
77
  "table",
@@ -82,6 +83,7 @@ DIRECT_TYPES = [
82
83
  "query",
83
84
  "tools",
84
85
  "mcp",
86
+ "model",
85
87
  ]
86
88
 
87
89
 
@@ -0,0 +1,79 @@
1
+ """Security utilities for mustache template processing."""
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ # Regex pattern for simple variables only - same as frontend
7
+ SIMPLE_VARIABLE_PATTERN = re.compile(r"\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}")
8
+
9
+ # Patterns for complex mustache syntax that we want to block
10
+ DANGEROUS_PATTERNS = [
11
+ re.compile(r"\{\{\{"), # Triple braces (unescaped HTML in Mustache)
12
+ re.compile(r"\{\{#"), # Conditionals/sections start
13
+ re.compile(r"\{\{/"), # Conditionals/sections end
14
+ re.compile(r"\{\{\^"), # Inverted sections
15
+ re.compile(r"\{\{&"), # Unescaped variables
16
+ re.compile(r"\{\{>"), # Partials
17
+ re.compile(r"\{\{!"), # Comments
18
+ re.compile(r"\{\{\."), # Current context
19
+ ]
20
+
21
+
22
+ def validate_mustache_template(template: str) -> None:
23
+ """Validate that a mustache template only contains simple variable substitutions.
24
+
25
+ Raises ValueError if complex mustache syntax is detected.
26
+ """
27
+ if not template:
28
+ return
29
+
30
+ # Check for dangerous patterns
31
+ for pattern in DANGEROUS_PATTERNS:
32
+ if pattern.search(template):
33
+ msg = (
34
+ "Complex mustache syntax is not allowed. Only simple variable substitution "
35
+ "like {{variable}} is permitted."
36
+ )
37
+ raise ValueError(msg)
38
+
39
+ # Check that all {{ }} patterns are simple variables
40
+ all_mustache_patterns = re.findall(r"\{\{[^}]*\}\}", template)
41
+ for pattern in all_mustache_patterns:
42
+ if not SIMPLE_VARIABLE_PATTERN.match(pattern):
43
+ msg = f"Invalid mustache variable: {pattern}. Only simple variable names like {{{{variable}}}} are allowed."
44
+ raise ValueError(msg)
45
+
46
+
47
+ def safe_mustache_render(template: str, variables: dict[str, Any]) -> str:
48
+ """Safely render a mustache template with only simple variable substitution.
49
+
50
+ This function performs a single-pass replacement of all {{variable}} patterns.
51
+ Variable values that themselves contain mustache-like patterns (e.g., "{{other}}")
52
+ will NOT be processed - they are treated as literal strings. This prevents
53
+ injection attacks where user-controlled values could introduce new template variables.
54
+
55
+ Args:
56
+ template: The mustache template string
57
+ variables: Dictionary of variables to substitute
58
+
59
+ Returns:
60
+ The rendered template
61
+
62
+ Raises:
63
+ ValueError: If template contains complex mustache syntax
64
+ """
65
+ # Validate template first
66
+ validate_mustache_template(template)
67
+
68
+ # Simple replacement - find all simple variables and replace them
69
+ def replace_variable(match):
70
+ var_name = match.group(1)
71
+
72
+ # Get the variable value directly (no dot notation support)
73
+ value = variables.get(var_name, "")
74
+
75
+ # Convert to string
76
+ return str(value) if value is not None else ""
77
+
78
+ # Replace all simple variables
79
+ return SIMPLE_VARIABLE_PATTERN.sub(replace_variable, template)
@@ -5,6 +5,20 @@ such as disabling certain features when running in Astra cloud environment.
5
5
  """
6
6
 
7
7
  import os
8
+ from typing import Any
9
+
10
+
11
+ def is_astra_cloud_environment() -> bool:
12
+ """Check if we're running in an Astra cloud environment.
13
+
14
+ Check if the environment variable ASTRA_CLOUD_DISABLE_COMPONENT is set to true.
15
+ IF it is, then we know we are in an Astra cloud environment.
16
+
17
+ Returns:
18
+ bool: True if running in an Astra cloud environment, False otherwise.
19
+ """
20
+ disable_component = os.getenv("ASTRA_CLOUD_DISABLE_COMPONENT", "false")
21
+ return disable_component.lower().strip() == "true"
8
22
 
9
23
 
10
24
  def raise_error_if_astra_cloud_disable_component(msg: str):
@@ -20,7 +34,71 @@ def raise_error_if_astra_cloud_disable_component(msg: str):
20
34
  Raises:
21
35
  ValueError: If running in an Astra cloud environment.
22
36
  """
23
- if (
24
- disable_component := os.getenv("ASTRA_CLOUD_DISABLE_COMPONENT", "false")
25
- ) and disable_component.lower().strip() == "true":
37
+ if is_astra_cloud_environment():
26
38
  raise ValueError(msg)
39
+
40
+
41
+ # Mapping of component types to their disabled module names and component names when in Astra cloud environment.
42
+ # Keys are component type names (e.g., "docling")
43
+ # Values are sets containing both module filenames (e.g., "chunk_docling_document")
44
+ # and component names (e.g., "ChunkDoclingDocument")
45
+ # To add new disabled components in the future, simply add entries to this dictionary.
46
+ ASTRA_CLOUD_DISABLED_COMPONENTS: dict[str, set[str]] = {
47
+ "docling": {
48
+ # Module filenames (for dynamic loading)
49
+ "chunk_docling_document",
50
+ "docling_inline",
51
+ "export_docling_document",
52
+ # Component names (for index/cache loading)
53
+ "ChunkDoclingDocument",
54
+ "DoclingInline",
55
+ "ExportDoclingDocument",
56
+ }
57
+ }
58
+
59
+
60
+ def is_component_disabled_in_astra_cloud(component_type: str, module_filename: str) -> bool:
61
+ """Check if a specific component module should be disabled in cloud environment.
62
+
63
+ Args:
64
+ component_type: The top-level component type (e.g., "docling")
65
+ module_filename: The module filename without extension (e.g., "chunk_docling_document")
66
+
67
+ Returns:
68
+ bool: True if the component should be disabled, False otherwise.
69
+ """
70
+ if not is_astra_cloud_environment():
71
+ return False
72
+
73
+ disabled_modules = ASTRA_CLOUD_DISABLED_COMPONENTS.get(component_type.lower(), set())
74
+ return module_filename in disabled_modules
75
+
76
+
77
+ def filter_disabled_components_from_dict(modules_dict: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
78
+ """Filter out disabled components from a loaded modules dictionary.
79
+
80
+ This function is used to filter components that were loaded from index/cache,
81
+ since those bypass the dynamic loading filter.
82
+
83
+ Args:
84
+ modules_dict: Dictionary mapping component types to their components
85
+
86
+ Returns:
87
+ Filtered dictionary with disabled components removed
88
+ """
89
+ if not is_astra_cloud_environment():
90
+ return modules_dict
91
+
92
+ filtered_dict: dict[str, dict[str, Any]] = {}
93
+ for component_type, components in modules_dict.items():
94
+ disabled_set = ASTRA_CLOUD_DISABLED_COMPONENTS.get(component_type.lower(), set())
95
+ if disabled_set:
96
+ # Filter out disabled components
97
+ filtered_components = {name: comp for name, comp in components.items() if name not in disabled_set}
98
+ if filtered_components: # Only add if there are remaining components
99
+ filtered_dict[component_type] = filtered_components
100
+ else:
101
+ # No disabled components for this type, keep all
102
+ filtered_dict[component_type] = components
103
+
104
+ return filtered_dict
@@ -1,14 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lfx-nightly
3
- Version: 0.2.0.dev41
3
+ Version: 0.3.0.dev3
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.14,>=3.10
7
+ Requires-Dist: ag-ui-protocol>=0.1.10
7
8
  Requires-Dist: aiofile<4.0.0,>=3.8.0
8
9
  Requires-Dist: aiofiles<25.0.0,>=24.1.0
9
10
  Requires-Dist: asyncer<1.0.0,>=0.0.8
10
11
  Requires-Dist: cachetools>=6.0.0
11
12
  Requires-Dist: chardet<6.0.0,>=5.2.0
13
+ Requires-Dist: cryptography>=43.0.0
12
14
  Requires-Dist: defusedxml<1.0.0,>=0.7.1
13
15
  Requires-Dist: docstring-parser<1.0.0,>=0.16
14
16
  Requires-Dist: emoji<3.0.0,>=2.14.1
@@ -257,7 +259,10 @@ async def get_graph() -> Graph:
257
259
  uv pip install lfx
258
260
 
259
261
  # Install additional dependencies required for the agent
260
- uv pip install langchain-community langchain beautifulsoup4 lxml langchain-openai
262
+ uv pip install 'langchain-core>=0.3.0,<1.0.0' \
263
+ 'langchain-openai>=0.3.0,<1.0.0' \
264
+ 'langchain-community>=0.3.0,<1.0.0' \
265
+ beautifulsoup4 lxml
261
266
  ```
262
267
 
263
268
  **Step 3: Set up environment**