lfx-nightly 0.1.12.dev26__py3-none-any.whl → 0.1.12.dev28__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.
@@ -182,6 +182,8 @@ class ParameterHandler:
182
182
  params = self._handle_code_field(field_name, val, params)
183
183
  elif field.get("type") in {"dict", "NestedDict"}:
184
184
  params = self._handle_dict_field(field_name, val, params)
185
+ elif field.get("type") == "table":
186
+ params = self._handle_table_field(field_name, val, params, load_from_db_fields)
185
187
  else:
186
188
  params = self._handle_other_direct_types(field_name, field, val, params)
187
189
 
@@ -190,6 +192,53 @@ class ParameterHandler:
190
192
 
191
193
  return params, load_from_db_fields
192
194
 
195
+ def _handle_table_field(
196
+ self,
197
+ field_name: str,
198
+ val: Any,
199
+ params: dict[str, Any],
200
+ load_from_db_fields: list[str] | None = None,
201
+ ) -> dict[str, Any]:
202
+ """Handle table field type with load_from_db column support."""
203
+ if load_from_db_fields is None:
204
+ load_from_db_fields = []
205
+ if val is None:
206
+ params[field_name] = []
207
+ return params
208
+
209
+ # Store the table data as-is for now
210
+ # The actual column processing will happen in the loading phase
211
+ if isinstance(val, list) and all(isinstance(item, dict) for item in val):
212
+ params[field_name] = val
213
+ else:
214
+ msg = f"Invalid value type {type(val)} for table field {field_name}"
215
+ raise ValueError(msg)
216
+
217
+ # Get table schema from the field to identify load_from_db columns
218
+ field_template = self.template_dict.get(field_name, {})
219
+ table_schema = field_template.get("table_schema", [])
220
+
221
+ # Track which columns need database loading
222
+ load_from_db_columns = []
223
+ for column_schema in table_schema:
224
+ if isinstance(column_schema, dict) and column_schema.get("load_from_db"):
225
+ load_from_db_columns.append(column_schema["name"])
226
+ elif hasattr(column_schema, "load_from_db") and column_schema.load_from_db:
227
+ load_from_db_columns.append(column_schema.name)
228
+
229
+ # Store metadata for later processing
230
+ if load_from_db_columns:
231
+ # Store table column metadata for the loading phase
232
+ table_load_metadata_key = f"{field_name}_load_from_db_columns"
233
+ params[table_load_metadata_key] = load_from_db_columns
234
+
235
+ # Add to load_from_db_fields so it gets processed
236
+ # We'll use a special naming convention to identify table fields
237
+ load_from_db_fields.append(f"table:{field_name}")
238
+ self.load_from_db_fields.append(f"table:{field_name}")
239
+
240
+ return params
241
+
193
242
  def handle_optional_field(self, field_name: str, field: dict, params: dict[str, Any]) -> None:
194
243
  """Handle optional fields."""
195
244
  if not field.get("required") and params.get(field_name) is None:
@@ -126,6 +126,88 @@ def load_from_env_vars(params, load_from_db_fields):
126
126
  return params
127
127
 
128
128
 
129
+ async def update_table_params_with_load_from_db_fields(
130
+ custom_component: CustomComponent,
131
+ params: dict,
132
+ table_field_name: str,
133
+ *,
134
+ fallback_to_env_vars: bool = False,
135
+ ) -> dict:
136
+ """Update table parameters with load_from_db column values."""
137
+ # Get the table data and column metadata
138
+ table_data = params.get(table_field_name, [])
139
+ metadata_key = f"{table_field_name}_load_from_db_columns"
140
+ load_from_db_columns = params.pop(metadata_key, [])
141
+
142
+ if not table_data or not load_from_db_columns:
143
+ return params
144
+
145
+ async with session_scope() as session:
146
+ settings_service = get_settings_service()
147
+ is_noop_session = isinstance(session, NoopSession) or (
148
+ settings_service and settings_service.settings.use_noop_database
149
+ )
150
+
151
+ # Process each row in the table
152
+ updated_table_data = []
153
+ for row in table_data:
154
+ if not isinstance(row, dict):
155
+ updated_table_data.append(row)
156
+ continue
157
+
158
+ updated_row = row.copy()
159
+
160
+ # Process each column that needs database loading
161
+ for column_name in load_from_db_columns:
162
+ if column_name not in updated_row:
163
+ continue
164
+
165
+ # The column value should be the name of the global variable to lookup
166
+ variable_name = updated_row[column_name]
167
+ if not variable_name:
168
+ continue
169
+
170
+ try:
171
+ if is_noop_session:
172
+ # Fallback to environment variables
173
+ key = os.getenv(variable_name)
174
+ if key:
175
+ logger.info(f"Using environment variable {variable_name} for table column {column_name}")
176
+ else:
177
+ logger.error(f"Environment variable {variable_name} is not set.")
178
+ else:
179
+ # Load from database
180
+ key = await custom_component.get_variable(
181
+ name=variable_name, field=f"{table_field_name}.{column_name}", session=session
182
+ )
183
+
184
+ except ValueError as e:
185
+ if "User id is not set" in str(e):
186
+ raise
187
+ logger.debug(str(e))
188
+ key = None
189
+
190
+ # If we couldn't get from database and fallback is enabled, try environment
191
+ if fallback_to_env_vars and key is None:
192
+ key = os.getenv(variable_name)
193
+ if key:
194
+ logger.info(f"Using environment variable {variable_name} for table column {column_name}")
195
+ else:
196
+ logger.error(f"Environment variable {variable_name} is not set.")
197
+
198
+ # Update the column value with the resolved value
199
+ updated_row[column_name] = key if key is not None else None
200
+ if key is None:
201
+ logger.warning(
202
+ f"Could not get value for {variable_name} in table column {column_name}. Setting it to None."
203
+ )
204
+
205
+ updated_table_data.append(updated_row)
206
+
207
+ params[table_field_name] = updated_table_data
208
+ return params
209
+
210
+
129
211
  async def update_params_with_load_from_db_fields(
130
212
  custom_component: CustomComponent,
131
213
  params,
@@ -142,27 +224,38 @@ async def update_params_with_load_from_db_fields(
142
224
  logger.debug("Loading variables from environment variables because database is not available.")
143
225
  return load_from_env_vars(params, load_from_db_fields)
144
226
  for field in load_from_db_fields:
145
- if field not in params or not params[field]:
146
- continue
147
-
148
- try:
149
- key = await custom_component.get_variable(name=params[field], field=field, session=session)
150
- except ValueError as e:
151
- if any(reason in str(e) for reason in ["User id is not set", "variable not found."]):
152
- raise
153
- logger.debug(str(e))
154
- key = None
155
-
156
- if fallback_to_env_vars and key is None:
157
- key = os.getenv(params[field])
158
- if key:
159
- logger.info(f"Using environment variable {params[field]} for {field}")
160
- else:
161
- logger.error(f"Environment variable {params[field]} is not set.")
162
-
163
- params[field] = key if key is not None else None
164
- if key is None:
165
- logger.warning(f"Could not get value for {field}. Setting it to None.")
227
+ # Check if this is a table field (using our naming convention)
228
+ if field.startswith("table:"):
229
+ table_field_name = field[6:] # Remove "table:" prefix
230
+ params = await update_table_params_with_load_from_db_fields(
231
+ custom_component,
232
+ params,
233
+ table_field_name,
234
+ fallback_to_env_vars=fallback_to_env_vars,
235
+ )
236
+ else:
237
+ # Handle regular field-level load_from_db
238
+ if field not in params or not params[field]:
239
+ continue
240
+
241
+ try:
242
+ key = await custom_component.get_variable(name=params[field], field=field, session=session)
243
+ except ValueError as e:
244
+ if any(reason in str(e) for reason in ["User id is not set", "variable not found."]):
245
+ raise
246
+ logger.debug(str(e))
247
+ key = None
248
+
249
+ if fallback_to_env_vars and key is None:
250
+ key = os.getenv(params[field])
251
+ if key:
252
+ logger.info(f"Using environment variable {params[field]} for {field}")
253
+ else:
254
+ logger.error(f"Environment variable {params[field]} is not set.")
255
+
256
+ params[field] = key if key is not None else None
257
+ if key is None:
258
+ logger.warning(f"Could not get value for {field}. Setting it to None.")
166
259
 
167
260
  return params
168
261
 
lfx/schema/image.py CHANGED
@@ -23,6 +23,9 @@ def is_image_file(file_path) -> bool:
23
23
 
24
24
  def get_file_paths(files: list[str | dict]):
25
25
  """Get file paths for a list of files."""
26
+ if not files:
27
+ return []
28
+
26
29
  storage_service = get_storage_service()
27
30
  if not storage_service:
28
31
  # Extract paths from dicts if present
@@ -31,7 +34,12 @@ def get_file_paths(files: list[str | dict]):
31
34
  cache_dir = Path(user_cache_dir("langflow"))
32
35
 
33
36
  for file in files:
37
+ if not file: # Skip empty/None files
38
+ continue
39
+
34
40
  file_path = file["path"] if isinstance(file, dict) and "path" in file else file
41
+ if not file_path: # Skip empty paths
42
+ continue
35
43
 
36
44
  # If it's a relative path like "flow_id/filename", resolve it to cache dir
37
45
  path = Path(file_path)
@@ -52,13 +60,30 @@ def get_file_paths(files: list[str | dict]):
52
60
  # Handle dict case
53
61
  if storage_service is None:
54
62
  continue
63
+
64
+ if not file: # Skip empty/None files
65
+ continue
66
+
55
67
  if isinstance(file, dict) and "path" in file:
56
- file_path = Path(file["path"])
68
+ file_path_str = file["path"]
57
69
  elif hasattr(file, "path") and file.path:
58
- file_path = Path(file.path)
70
+ file_path_str = file.path
59
71
  else:
60
- file_path = Path(file)
61
- flow_id, file_name = str(file_path.parent), file_path.name
72
+ file_path_str = file
73
+
74
+ if not file_path_str: # Skip empty paths
75
+ continue
76
+
77
+ file_path = Path(file_path_str)
78
+ # Handle edge case where path might be just a filename without parent
79
+ if file_path.parent == Path():
80
+ flow_id, file_name = "", file_path.name
81
+ else:
82
+ flow_id, file_name = str(file_path.parent), file_path.name
83
+
84
+ if not file_name: # Skip if no filename
85
+ continue
86
+
62
87
  file_paths.append(storage_service.build_full_path(flow_id=flow_id, file_name=file_name))
63
88
  return file_paths
64
89
 
@@ -69,22 +94,31 @@ async def get_files(
69
94
  convert_to_base64: bool = False,
70
95
  ):
71
96
  """Get files from storage service."""
97
+ if not file_paths:
98
+ return []
99
+
72
100
  storage_service = get_storage_service()
73
101
  if not storage_service:
74
102
  # For testing purposes, read files directly when no storage service
75
103
  file_objects: list[str | bytes] = []
76
104
  for file_path_str in file_paths:
105
+ if not file_path_str: # Skip empty paths
106
+ continue
107
+
77
108
  file_path = Path(file_path_str)
78
109
  if file_path.exists():
79
110
  # Use async read for compatibility
80
-
81
- async with aiofiles.open(file_path, "rb") as f:
82
- file_content = await f.read()
83
- if convert_to_base64:
84
- file_base64 = base64.b64encode(file_content).decode("utf-8")
85
- file_objects.append(file_base64)
86
- else:
87
- file_objects.append(file_content)
111
+ try:
112
+ async with aiofiles.open(file_path, "rb") as f:
113
+ file_content = await f.read()
114
+ if convert_to_base64:
115
+ file_base64 = base64.b64encode(file_content).decode("utf-8")
116
+ file_objects.append(file_base64)
117
+ else:
118
+ file_objects.append(file_content)
119
+ except Exception as e:
120
+ msg = f"Error reading file {file_path}: {e}"
121
+ raise FileNotFoundError(msg) from e
88
122
  else:
89
123
  msg = f"File not found: {file_path}"
90
124
  raise FileNotFoundError(msg)
@@ -92,16 +126,32 @@ async def get_files(
92
126
 
93
127
  file_objects: list[str | bytes] = []
94
128
  for file in file_paths:
129
+ if not file: # Skip empty file paths
130
+ continue
131
+
95
132
  file_path = Path(file)
96
- flow_id, file_name = str(file_path.parent), file_path.name
133
+ # Handle edge case where path might be just a filename without parent
134
+ if file_path.parent == Path():
135
+ flow_id, file_name = "", file_path.name
136
+ else:
137
+ flow_id, file_name = str(file_path.parent), file_path.name
138
+
139
+ if not file_name: # Skip if no filename
140
+ continue
141
+
97
142
  if not storage_service:
98
143
  continue
99
- file_object = await storage_service.get_file(flow_id=flow_id, file_name=file_name)
100
- if convert_to_base64:
101
- file_base64 = base64.b64encode(file_object).decode("utf-8")
102
- file_objects.append(file_base64)
103
- else:
104
- file_objects.append(file_object)
144
+
145
+ try:
146
+ file_object = await storage_service.get_file(flow_id=flow_id, file_name=file_name)
147
+ if convert_to_base64:
148
+ file_base64 = base64.b64encode(file_object).decode("utf-8")
149
+ file_objects.append(file_base64)
150
+ else:
151
+ file_objects.append(file_object)
152
+ except Exception as e:
153
+ msg = f"Error getting file {file} from storage: {e}"
154
+ raise FileNotFoundError(msg) from e
105
155
  return file_objects
106
156
 
107
157
 
@@ -115,6 +165,9 @@ class Image(BaseModel):
115
165
  """Convert image to base64 string."""
116
166
  if self.path:
117
167
  files = get_files([self.path], convert_to_base64=True)
168
+ if not files:
169
+ msg = f"No files found or file could not be converted to base64: {self.path}"
170
+ raise ValueError(msg)
118
171
  return files[0]
119
172
  msg = "Image path is not set."
120
173
  raise ValueError(msg)
lfx/schema/message.py CHANGED
@@ -139,7 +139,8 @@ class Message(Data):
139
139
  if self.sender == MESSAGE_SENDER_USER or not self.sender:
140
140
  if self.files:
141
141
  contents = [{"type": "text", "text": text}]
142
- contents.extend(self.get_file_content_dicts())
142
+ file_contents = self.get_file_content_dicts()
143
+ contents.extend(file_contents)
143
144
  human_message = HumanMessage(content=contents)
144
145
  else:
145
146
  human_message = HumanMessage(content=text)
@@ -198,7 +199,11 @@ class Message(Data):
198
199
  # Keep this async method for backwards compatibility
199
200
  def get_file_content_dicts(self):
200
201
  content_dicts = []
201
- files = get_file_paths(self.files)
202
+ try:
203
+ files = get_file_paths(self.files)
204
+ except Exception as e: # noqa: BLE001
205
+ logger.error(f"Error getting file paths: {e}")
206
+ return content_dicts
202
207
 
203
208
  for file in files:
204
209
  if isinstance(file, Image):
lfx/schema/table.py CHANGED
@@ -44,6 +44,8 @@ class Column(BaseModel):
44
44
  disable_edit: bool = Field(default=False)
45
45
  edit_mode: EditMode | None = Field(default=EditMode.POPOVER)
46
46
  hidden: bool = Field(default=False)
47
+ load_from_db: bool = Field(default=False)
48
+ """Whether this column's default value should be loaded from global variables"""
47
49
 
48
50
  @model_validator(mode="after")
49
51
  def set_display_name(self):
@@ -226,6 +226,10 @@ class Settings(BaseSettings):
226
226
  """The host on which Langflow will run."""
227
227
  port: int = 7860
228
228
  """The port on which Langflow will run."""
229
+ runtime_port: int | None = Field(default=None, exclude=True)
230
+ """TEMPORARY: The port detected at runtime after checking for conflicts.
231
+ This field is system-managed only and will be removed in future versions
232
+ when strict port enforcement is implemented (errors will be raised if port unavailable)."""
229
233
  workers: int = 1
230
234
  """The number of workers to run."""
231
235
  log_level: str = "critical"
@@ -275,6 +279,9 @@ class Settings(BaseSettings):
275
279
  mcp_server_enable_progress_notifications: bool = False
276
280
  """If set to False, Langflow will not send progress notifications in the MCP server."""
277
281
 
282
+ # Add projects to MCP servers automatically on creation
283
+ add_projects_to_mcp_servers: bool = True
284
+ """If set to True, newly created projects will be added to the user's MCP servers config automatically."""
278
285
  # MCP Composer
279
286
  mcp_composer_enabled: bool = True
280
287
  """If set to False, Langflow will not start the MCP Composer service."""
lfx/utils/util.py CHANGED
@@ -2,6 +2,7 @@ import difflib
2
2
  import importlib
3
3
  import inspect
4
4
  import json
5
+ import os
5
6
  import re
6
7
  from functools import wraps
7
8
  from pathlib import Path
@@ -16,6 +17,140 @@ from lfx.template.frontend_node.constants import FORCE_SHOW_FIELDS
16
17
  from lfx.utils import constants
17
18
 
18
19
 
20
+ def detect_container_environment() -> str | None:
21
+ """Detect if running in a container and return the appropriate container type.
22
+
23
+ Returns:
24
+ 'docker' if running in Docker, 'podman' if running in Podman, None otherwise.
25
+ """
26
+ # Check for .dockerenv file (Docker)
27
+ if Path("/.dockerenv").exists():
28
+ return "docker"
29
+
30
+ # Check cgroup for container indicators
31
+ try:
32
+ with Path("/proc/self/cgroup").open() as f:
33
+ content = f.read()
34
+ if "docker" in content:
35
+ return "docker"
36
+ if "podman" in content:
37
+ return "podman"
38
+ except (FileNotFoundError, PermissionError):
39
+ pass
40
+
41
+ # Check environment variables (lowercase 'container' is the standard for Podman)
42
+ if os.getenv("container") == "podman": # noqa: SIM112
43
+ return "podman"
44
+
45
+ return None
46
+
47
+
48
+ def get_container_host() -> str | None:
49
+ """Get the hostname to access host services from within a container.
50
+
51
+ Tries multiple methods to find the correct hostname:
52
+ 1. host.containers.internal (Podman) or host.docker.internal (Docker)
53
+ 2. Gateway IP from routing table (fallback for Linux)
54
+
55
+ Returns:
56
+ The hostname or IP to use, or None if not in a container.
57
+ """
58
+ import socket
59
+
60
+ # Check if we're in a container first
61
+ container_type = detect_container_environment()
62
+ if not container_type:
63
+ return None
64
+
65
+ # Try container-specific hostnames first based on detected type
66
+ if container_type == "podman":
67
+ # Podman: try host.containers.internal first
68
+ try:
69
+ socket.getaddrinfo("host.containers.internal", None)
70
+ except socket.gaierror:
71
+ pass
72
+ else:
73
+ return "host.containers.internal"
74
+
75
+ # Fallback to host.docker.internal (for Podman Desktop on macOS)
76
+ try:
77
+ socket.getaddrinfo("host.docker.internal", None)
78
+ except socket.gaierror:
79
+ pass
80
+ else:
81
+ return "host.docker.internal"
82
+ else:
83
+ # Docker: try host.docker.internal first
84
+ try:
85
+ socket.getaddrinfo("host.docker.internal", None)
86
+ except socket.gaierror:
87
+ pass
88
+ else:
89
+ return "host.docker.internal"
90
+
91
+ # Fallback to host.containers.internal (unlikely but possible)
92
+ try:
93
+ socket.getaddrinfo("host.containers.internal", None)
94
+ except socket.gaierror:
95
+ pass
96
+ else:
97
+ return "host.containers.internal"
98
+
99
+ # Fallback: try to get gateway IP from routing table (Linux containers)
100
+ try:
101
+ with Path("/proc/net/route").open() as f:
102
+ for line in f:
103
+ fields = line.strip().split()
104
+ min_field_count = 3 # Minimum fields needed: interface, destination, gateway
105
+ if len(fields) >= min_field_count and fields[1] == "00000000": # Default route
106
+ # Gateway is in hex format (little-endian)
107
+ gateway_hex = fields[2]
108
+ # Convert hex to IP address
109
+ # The hex is in little-endian format, so we read it backwards in pairs
110
+ octets = [gateway_hex[i : i + 2] for i in range(0, 8, 2)]
111
+ return ".".join(str(int(octet, 16)) for octet in reversed(octets))
112
+ except (FileNotFoundError, PermissionError, IndexError, ValueError):
113
+ pass
114
+
115
+ return None
116
+
117
+
118
+ def transform_localhost_url(url: str) -> str:
119
+ """Transform localhost URLs to container-accessible hosts when running in a container.
120
+
121
+ Automatically detects if running inside a container and finds the appropriate host
122
+ address to replace localhost/127.0.0.1. Tries in order:
123
+ - host.docker.internal (if resolvable)
124
+ - host.containers.internal (if resolvable)
125
+ - Gateway IP from routing table (fallback)
126
+
127
+ Args:
128
+ url: The original URL
129
+
130
+ Returns:
131
+ Transformed URL with container-accessible host if applicable, otherwise the original URL.
132
+
133
+ Example:
134
+ >>> transform_localhost_url("http://localhost:5001")
135
+ # Returns "http://host.docker.internal:5001" if running in Docker and hostname resolves
136
+ # Returns "http://172.17.0.1:5001" if running in Docker on Linux (gateway IP fallback)
137
+ # Returns "http://localhost:5001" if not in a container
138
+ """
139
+ container_host = get_container_host()
140
+
141
+ if not container_host:
142
+ return url
143
+
144
+ # Replace localhost and 127.0.0.1 with the container host
145
+ localhost_patterns = ["localhost", "127.0.0.1"]
146
+
147
+ for pattern in localhost_patterns:
148
+ if pattern in url:
149
+ return url.replace(pattern, container_host)
150
+
151
+ return url
152
+
153
+
19
154
  def unescape_string(s: str):
20
155
  # Replace escaped new line characters with actual new line characters
21
156
  return s.replace("\\n", "\n")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lfx-nightly
3
- Version: 0.1.12.dev26
3
+ Version: 0.1.12.dev28
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