lfx-nightly 0.2.0.dev26__py3-none-any.whl → 0.2.1.dev7__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 (85) hide show
  1. lfx/_assets/component_index.json +1 -1
  2. lfx/base/agents/agent.py +9 -4
  3. lfx/base/agents/altk_base_agent.py +16 -3
  4. lfx/base/agents/altk_tool_wrappers.py +1 -1
  5. lfx/base/agents/utils.py +4 -0
  6. lfx/base/composio/composio_base.py +78 -41
  7. lfx/base/data/base_file.py +14 -4
  8. lfx/base/data/cloud_storage_utils.py +156 -0
  9. lfx/base/data/docling_utils.py +191 -65
  10. lfx/base/data/storage_utils.py +109 -0
  11. lfx/base/datastax/astradb_base.py +75 -64
  12. lfx/base/mcp/util.py +2 -2
  13. lfx/base/models/__init__.py +11 -1
  14. lfx/base/models/anthropic_constants.py +21 -12
  15. lfx/base/models/google_generative_ai_constants.py +33 -9
  16. lfx/base/models/model_metadata.py +6 -0
  17. lfx/base/models/ollama_constants.py +196 -30
  18. lfx/base/models/openai_constants.py +37 -10
  19. lfx/base/models/unified_models.py +1123 -0
  20. lfx/base/models/watsonx_constants.py +36 -0
  21. lfx/base/tools/component_tool.py +2 -9
  22. lfx/cli/commands.py +6 -1
  23. lfx/cli/run.py +65 -409
  24. lfx/cli/script_loader.py +13 -3
  25. lfx/components/__init__.py +0 -3
  26. lfx/components/composio/github_composio.py +1 -1
  27. lfx/components/cuga/cuga_agent.py +39 -27
  28. lfx/components/data_source/api_request.py +4 -2
  29. lfx/components/docling/__init__.py +45 -11
  30. lfx/components/docling/chunk_docling_document.py +3 -1
  31. lfx/components/docling/docling_inline.py +39 -49
  32. lfx/components/docling/export_docling_document.py +3 -1
  33. lfx/components/elastic/opensearch_multimodal.py +215 -57
  34. lfx/components/files_and_knowledge/file.py +439 -39
  35. lfx/components/files_and_knowledge/ingestion.py +8 -0
  36. lfx/components/files_and_knowledge/retrieval.py +10 -0
  37. lfx/components/files_and_knowledge/save_file.py +123 -53
  38. lfx/components/ibm/watsonx.py +7 -1
  39. lfx/components/input_output/chat_output.py +7 -1
  40. lfx/components/langchain_utilities/tool_calling.py +14 -6
  41. lfx/components/llm_operations/batch_run.py +80 -25
  42. lfx/components/llm_operations/lambda_filter.py +33 -6
  43. lfx/components/llm_operations/llm_conditional_router.py +39 -7
  44. lfx/components/llm_operations/structured_output.py +38 -12
  45. lfx/components/models/__init__.py +16 -74
  46. lfx/components/models_and_agents/agent.py +51 -201
  47. lfx/components/models_and_agents/embedding_model.py +185 -339
  48. lfx/components/models_and_agents/language_model.py +54 -318
  49. lfx/components/models_and_agents/mcp_component.py +58 -9
  50. lfx/components/ollama/ollama.py +9 -4
  51. lfx/components/ollama/ollama_embeddings.py +2 -1
  52. lfx/components/openai/openai_chat_model.py +1 -1
  53. lfx/components/processing/__init__.py +0 -3
  54. lfx/components/vllm/__init__.py +37 -0
  55. lfx/components/vllm/vllm.py +141 -0
  56. lfx/components/vllm/vllm_embeddings.py +110 -0
  57. lfx/custom/custom_component/custom_component.py +8 -6
  58. lfx/custom/directory_reader/directory_reader.py +5 -2
  59. lfx/graph/utils.py +64 -18
  60. lfx/inputs/__init__.py +2 -0
  61. lfx/inputs/input_mixin.py +54 -0
  62. lfx/inputs/inputs.py +115 -0
  63. lfx/interface/initialize/loading.py +42 -12
  64. lfx/io/__init__.py +2 -0
  65. lfx/run/__init__.py +5 -0
  66. lfx/run/base.py +494 -0
  67. lfx/schema/data.py +1 -1
  68. lfx/schema/image.py +28 -19
  69. lfx/schema/message.py +19 -3
  70. lfx/services/interfaces.py +5 -0
  71. lfx/services/manager.py +5 -4
  72. lfx/services/mcp_composer/service.py +45 -13
  73. lfx/services/settings/auth.py +18 -11
  74. lfx/services/settings/base.py +12 -24
  75. lfx/services/settings/constants.py +2 -0
  76. lfx/services/storage/local.py +37 -0
  77. lfx/services/storage/service.py +19 -0
  78. lfx/utils/constants.py +1 -0
  79. lfx/utils/image.py +29 -11
  80. lfx/utils/validate_cloud.py +14 -3
  81. {lfx_nightly-0.2.0.dev26.dist-info → lfx_nightly-0.2.1.dev7.dist-info}/METADATA +5 -2
  82. {lfx_nightly-0.2.0.dev26.dist-info → lfx_nightly-0.2.1.dev7.dist-info}/RECORD +84 -78
  83. lfx/components/processing/dataframe_to_toolset.py +0 -259
  84. {lfx_nightly-0.2.0.dev26.dist-info → lfx_nightly-0.2.1.dev7.dist-info}/WHEEL +0 -0
  85. {lfx_nightly-0.2.0.dev26.dist-info → lfx_nightly-0.2.1.dev7.dist-info}/entry_points.txt +0 -0
lfx/schema/image.py CHANGED
@@ -7,6 +7,7 @@ from platformdirs import user_cache_dir
7
7
  from pydantic import BaseModel
8
8
 
9
9
  from lfx.services.deps import get_storage_service
10
+ from lfx.utils.image import create_image_content_dict
10
11
 
11
12
  IMAGE_ENDPOINT = "/files/images/"
12
13
 
@@ -37,7 +38,14 @@ def get_file_paths(files: list[str | dict]):
37
38
  if not file: # Skip empty/None files
38
39
  continue
39
40
 
40
- file_path = file["path"] if isinstance(file, dict) and "path" in file else file
41
+ # Handle Image objects, dicts, and strings
42
+ if isinstance(file, dict) and "path" in file:
43
+ file_path = file["path"]
44
+ elif hasattr(file, "path") and file.path:
45
+ file_path = file.path
46
+ else:
47
+ file_path = file
48
+
41
49
  if not file_path: # Skip empty paths
42
50
  continue
43
51
 
@@ -74,12 +82,7 @@ def get_file_paths(files: list[str | dict]):
74
82
  if not file_path_str: # Skip empty paths
75
83
  continue
76
84
 
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
85
+ flow_id, file_name = storage_service.parse_file_path(file_path_str)
83
86
 
84
87
  if not file_name: # Skip if no filename
85
88
  continue
@@ -129,12 +132,7 @@ async def get_files(
129
132
  if not file: # Skip empty file paths
130
133
  continue
131
134
 
132
- file_path = Path(file)
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
135
+ flow_id, file_name = storage_service.parse_file_path(file)
138
136
 
139
137
  if not file_name: # Skip if no filename
140
138
  continue
@@ -172,12 +170,23 @@ class Image(BaseModel):
172
170
  msg = "Image path is not set."
173
171
  raise ValueError(msg)
174
172
 
175
- def to_content_dict(self):
176
- """Convert image to content dictionary."""
177
- return {
178
- "type": "image_url",
179
- "image_url": self.to_base64(),
180
- }
173
+ def to_content_dict(self, flow_id: str | None = None):
174
+ """Convert image to content dictionary.
175
+
176
+ Args:
177
+ flow_id: Optional flow ID to prepend to the path if it doesn't contain one
178
+ """
179
+ if not self.path:
180
+ msg = "Image path is not set."
181
+ raise ValueError(msg)
182
+
183
+ # If the path doesn't contain a "/" and we have a flow_id, prepend it
184
+ image_path = self.path
185
+ if flow_id and "/" not in self.path:
186
+ image_path = f"{flow_id}/{self.path}"
187
+
188
+ # Use the utility function that properly handles the conversion
189
+ return create_image_content_dict(image_path, None, None)
181
190
 
182
191
  def get_url(self) -> str:
183
192
  """Get the URL for the image."""
lfx/schema/message.py CHANGED
@@ -109,7 +109,22 @@ class Message(Data):
109
109
  def model_post_init(self, /, _context: Any) -> None:
110
110
  new_files: list[Any] = []
111
111
  for file in self.files or []:
112
- if is_image_file(file):
112
+ # Skip if already an Image instance
113
+ if isinstance(file, Image):
114
+ new_files.append(file)
115
+ # Get the path string if file is a dict or has path attribute
116
+ elif isinstance(file, dict) and "path" in file:
117
+ file_path = file["path"]
118
+ if file_path and is_image_file(file_path):
119
+ new_files.append(Image(path=file_path))
120
+ else:
121
+ new_files.append(file_path if file_path else file)
122
+ elif hasattr(file, "path") and file.path:
123
+ if is_image_file(file.path):
124
+ new_files.append(Image(path=file.path))
125
+ else:
126
+ new_files.append(file.path)
127
+ elif isinstance(file, str) and is_image_file(file):
113
128
  new_files.append(Image(path=file))
114
129
  else:
115
130
  new_files.append(file)
@@ -213,7 +228,8 @@ class Message(Data):
213
228
 
214
229
  for file in files:
215
230
  if isinstance(file, Image):
216
- content_dicts.append(file.to_content_dict())
231
+ # Pass the message's flow_id to the Image for proper path resolution
232
+ content_dicts.append(file.to_content_dict(flow_id=self.flow_id))
217
233
  else:
218
234
  content_dicts.append(create_image_content_dict(file, None, model_name))
219
235
  return content_dicts
@@ -286,7 +302,7 @@ class Message(Data):
286
302
  @classmethod
287
303
  async def create(cls, **kwargs):
288
304
  """If files are present, create the message in a separate thread as is_image_file is blocking."""
289
- if "files" in kwargs:
305
+ if kwargs.get("files"):
290
306
  return await asyncio.to_thread(cls, **kwargs)
291
307
  return cls(**kwargs)
292
308
 
@@ -41,6 +41,11 @@ class StorageServiceProtocol(Protocol):
41
41
  """Build the full path of a file in the storage."""
42
42
  ...
43
43
 
44
+ @abstractmethod
45
+ def parse_file_path(self, full_path: str) -> tuple[str, str]:
46
+ """Parse a full storage path to extract flow_id and file_name."""
47
+ ...
48
+
44
49
 
45
50
  class SettingsServiceProtocol(Protocol):
46
51
  """Protocol for settings service."""
lfx/services/manager.py CHANGED
@@ -155,10 +155,11 @@ class ServiceManager:
155
155
  factories.append(obj())
156
156
  break
157
157
 
158
- except Exception as exc: # noqa: BLE001
159
- logger.debug(
160
- f"Could not initialize services. Please check your settings. Error in {name}.", exc_info=exc
161
- )
158
+ except Exception: # noqa: BLE001, S110
159
+ # This is expected during initial service discovery - some services
160
+ # may not have factories yet or depend on settings service being ready first
161
+ # Intentionally suppressed to avoid startup noise - not an error condition
162
+ pass
162
163
 
163
164
  return factories
164
165
 
@@ -73,7 +73,9 @@ class MCPComposerService(Service):
73
73
 
74
74
  def __init__(self):
75
75
  super().__init__()
76
- self.project_composers: dict[str, dict] = {} # project_id -> {process, host, port, sse_url, auth_config}
76
+ self.project_composers: dict[
77
+ str, dict
78
+ ] = {} # project_id -> {process, host, port, streamable_http_url, auth_config}
77
79
  self._start_locks: dict[
78
80
  str, asyncio.Lock
79
81
  ] = {} # Lock to prevent concurrent start operations for the same project
@@ -949,21 +951,24 @@ class MCPComposerService(Service):
949
951
  async def start_project_composer(
950
952
  self,
951
953
  project_id: str,
952
- sse_url: str,
954
+ streamable_http_url: str,
953
955
  auth_config: dict[str, Any] | None,
954
956
  max_retries: int = 3,
955
957
  max_startup_checks: int = 40,
956
958
  startup_delay: float = 2.0,
959
+ *,
960
+ legacy_sse_url: str | None = None,
957
961
  ) -> None:
958
962
  """Start an MCP Composer instance for a specific project.
959
963
 
960
964
  Args:
961
965
  project_id: The project ID
962
- sse_url: The SSE URL to connect to
966
+ streamable_http_url: Streamable HTTP endpoint for the remote Langflow MCP server
963
967
  auth_config: Authentication configuration
964
968
  max_retries: Maximum number of retry attempts (default: 3)
965
969
  max_startup_checks: Number of checks per retry attempt (default: 40)
966
970
  startup_delay: Delay between checks in seconds (default: 2.0)
971
+ legacy_sse_url: Optional legacy SSE URL used for backward compatibility
967
972
 
968
973
  Raises:
969
974
  MCPComposerError: Various specific errors if startup fails
@@ -994,7 +999,13 @@ class MCPComposerService(Service):
994
999
 
995
1000
  try:
996
1001
  await self._do_start_project_composer(
997
- project_id, sse_url, auth_config, max_retries, max_startup_checks, startup_delay
1002
+ project_id,
1003
+ streamable_http_url,
1004
+ auth_config,
1005
+ max_retries,
1006
+ max_startup_checks,
1007
+ startup_delay,
1008
+ legacy_sse_url=legacy_sse_url,
998
1009
  )
999
1010
  finally:
1000
1011
  # Clean up the task reference when done
@@ -1004,25 +1015,29 @@ class MCPComposerService(Service):
1004
1015
  async def _do_start_project_composer(
1005
1016
  self,
1006
1017
  project_id: str,
1007
- sse_url: str,
1018
+ streamable_http_url: str,
1008
1019
  auth_config: dict[str, Any] | None,
1009
1020
  max_retries: int = 3,
1010
1021
  max_startup_checks: int = 40,
1011
1022
  startup_delay: float = 2.0,
1023
+ *,
1024
+ legacy_sse_url: str | None = None,
1012
1025
  ) -> None:
1013
1026
  """Internal method to start an MCP Composer instance.
1014
1027
 
1015
1028
  Args:
1016
1029
  project_id: The project ID
1017
- sse_url: The SSE URL to connect to
1030
+ streamable_http_url: Streamable HTTP endpoint for the remote Langflow MCP server
1018
1031
  auth_config: Authentication configuration
1019
1032
  max_retries: Maximum number of retry attempts (default: 3)
1020
1033
  max_startup_checks: Number of checks per retry attempt (default: 40)
1021
1034
  startup_delay: Delay between checks in seconds (default: 2.0)
1035
+ legacy_sse_url: Optional legacy SSE URL used for backward compatibility
1022
1036
 
1023
1037
  Raises:
1024
1038
  MCPComposerError: Various specific errors if startup fails
1025
1039
  """
1040
+ legacy_sse_url = legacy_sse_url or f"{streamable_http_url.rstrip('/')}/sse"
1026
1041
  if not auth_config:
1027
1042
  no_auth_error_msg = "No auth settings provided"
1028
1043
  raise MCPComposerConfigError(no_auth_error_msg, project_id)
@@ -1126,10 +1141,11 @@ class MCPComposerService(Service):
1126
1141
  project_id,
1127
1142
  project_host,
1128
1143
  project_port,
1129
- sse_url,
1144
+ streamable_http_url,
1130
1145
  auth_config,
1131
1146
  max_startup_checks,
1132
1147
  startup_delay,
1148
+ legacy_sse_url=legacy_sse_url,
1133
1149
  )
1134
1150
 
1135
1151
  except MCPComposerError as e:
@@ -1174,7 +1190,9 @@ class MCPComposerService(Service):
1174
1190
  "process": process,
1175
1191
  "host": project_host,
1176
1192
  "port": project_port,
1177
- "sse_url": sse_url,
1193
+ "streamable_http_url": streamable_http_url,
1194
+ "legacy_sse_url": legacy_sse_url,
1195
+ "sse_url": legacy_sse_url,
1178
1196
  "auth_config": auth_config,
1179
1197
  }
1180
1198
  self._port_to_project[project_port] = project_id
@@ -1209,10 +1227,12 @@ class MCPComposerService(Service):
1209
1227
  project_id: str,
1210
1228
  host: str,
1211
1229
  port: int,
1212
- sse_url: str,
1230
+ streamable_http_url: str,
1213
1231
  auth_config: dict[str, Any] | None = None,
1214
1232
  max_startup_checks: int = 40,
1215
1233
  startup_delay: float = 2.0,
1234
+ *,
1235
+ legacy_sse_url: str | None = None,
1216
1236
  ) -> subprocess.Popen:
1217
1237
  """Start the MCP Composer subprocess for a specific project.
1218
1238
 
@@ -1220,10 +1240,11 @@ class MCPComposerService(Service):
1220
1240
  project_id: The project ID
1221
1241
  host: Host to bind to
1222
1242
  port: Port to bind to
1223
- sse_url: SSE URL to connect to
1243
+ streamable_http_url: Streamable HTTP endpoint to connect to
1224
1244
  auth_config: Authentication configuration
1225
1245
  max_startup_checks: Number of port binding checks (default: 40)
1226
1246
  startup_delay: Delay between checks in seconds (default: 2.0)
1247
+ legacy_sse_url: Optional legacy SSE URL used for backward compatibility when required by tooling
1227
1248
 
1228
1249
  Returns:
1229
1250
  The started subprocess
@@ -1232,6 +1253,9 @@ class MCPComposerService(Service):
1232
1253
  MCPComposerStartupError: If startup fails
1233
1254
  """
1234
1255
  settings = get_settings_service().settings
1256
+ # Some composer tooling still uses the --sse-url flag for backwards compatibility even in HTTP mode.
1257
+ effective_legacy_sse_url = legacy_sse_url or f"{streamable_http_url.rstrip('/')}/sse"
1258
+
1235
1259
  cmd = [
1236
1260
  "uvx",
1237
1261
  f"mcp-composer{settings.mcp_composer_version}",
@@ -1240,9 +1264,11 @@ class MCPComposerService(Service):
1240
1264
  "--host",
1241
1265
  host,
1242
1266
  "--mode",
1243
- "sse",
1267
+ "http",
1268
+ "--endpoint",
1269
+ streamable_http_url,
1244
1270
  "--sse-url",
1245
- sse_url,
1271
+ effective_legacy_sse_url,
1246
1272
  "--disable-composer-tools",
1247
1273
  ]
1248
1274
 
@@ -1266,7 +1292,7 @@ class MCPComposerService(Service):
1266
1292
  "oauth_host": "OAUTH_HOST",
1267
1293
  "oauth_port": "OAUTH_PORT",
1268
1294
  "oauth_server_url": "OAUTH_SERVER_URL",
1269
- "oauth_callback_path": "OAUTH_CALLBACK_PATH",
1295
+ "oauth_callback_url": "OAUTH_CALLBACK_URL",
1270
1296
  "oauth_client_id": "OAUTH_CLIENT_ID",
1271
1297
  "oauth_client_secret": "OAUTH_CLIENT_SECRET", # pragma: allowlist secret
1272
1298
  "oauth_auth_url": "OAUTH_AUTH_URL",
@@ -1275,6 +1301,12 @@ class MCPComposerService(Service):
1275
1301
  "oauth_provider_scope": "OAUTH_PROVIDER_SCOPE",
1276
1302
  }
1277
1303
 
1304
+ # Backwards compatibility: if oauth_callback_url not set, try oauth_callback_path
1305
+ if ("oauth_callback_url" not in auth_config or not auth_config.get("oauth_callback_url")) and (
1306
+ "oauth_callback_path" in auth_config and auth_config.get("oauth_callback_path")
1307
+ ):
1308
+ auth_config["oauth_callback_url"] = auth_config["oauth_callback_path"]
1309
+
1278
1310
  # Add environment variables as command line arguments
1279
1311
  # Only set non-empty values to avoid Pydantic validation errors
1280
1312
  for config_key, env_key in oauth_env_mapping.items():
@@ -27,6 +27,16 @@ class AuthSettings(BaseSettings):
27
27
  API_KEY_ALGORITHM: str = "HS256"
28
28
  API_V1_STR: str = "/api/v1"
29
29
 
30
+ # API Key Source Configuration
31
+ API_KEY_SOURCE: Literal["db", "env"] = Field(
32
+ default="db",
33
+ description=(
34
+ "Source for API key validation. "
35
+ "'db' validates against database-stored API keys (default behavior). "
36
+ "'env' validates against the LANGFLOW_API_KEY environment variable."
37
+ ),
38
+ )
39
+
30
40
  AUTO_LOGIN: bool = Field(
31
41
  default=True, # TODO: Set to False in v2.0
32
42
  description=(
@@ -115,19 +125,16 @@ class AuthSettings(BaseSettings):
115
125
  logger.debug("Secret key provided")
116
126
  secret_value = value.get_secret_value() if isinstance(value, SecretStr) else value
117
127
  write_secret_to_file(secret_key_path, secret_value)
118
- else:
119
- logger.debug("No secret key provided, generating a random one")
120
-
121
- if secret_key_path.exists():
122
- value = read_secret_from_file(secret_key_path)
123
- logger.debug("Loaded secret key")
124
- if not value:
125
- value = secrets.token_urlsafe(32)
126
- write_secret_to_file(secret_key_path, value)
127
- logger.debug("Saved secret key")
128
- else:
128
+ elif secret_key_path.exists():
129
+ value = read_secret_from_file(secret_key_path)
130
+ logger.debug("Loaded secret key")
131
+ if not value:
129
132
  value = secrets.token_urlsafe(32)
130
133
  write_secret_to_file(secret_key_path, value)
131
134
  logger.debug("Saved secret key")
135
+ else:
136
+ value = secrets.token_urlsafe(32)
137
+ write_secret_to_file(secret_key_path, value)
138
+ logger.debug("Saved secret key")
132
139
 
133
140
  return value if isinstance(value, SecretStr) else SecretStr(value).get_secret_value()
@@ -477,12 +477,10 @@ class Settings(BaseSettings):
477
477
  msg = f"Invalid database_url provided: '{value}'"
478
478
  raise ValueError(msg)
479
479
 
480
- logger.debug("No database_url provided, trying LANGFLOW_DATABASE_URL env variable")
481
480
  if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"):
482
481
  value = langflow_database_url
483
- logger.debug("Using LANGFLOW_DATABASE_URL env variable.")
482
+ logger.debug("Using LANGFLOW_DATABASE_URL env variable")
484
483
  else:
485
- logger.debug("No database_url env variable, using sqlite database")
486
484
  # Originally, we used sqlite:///./langflow.db
487
485
  # so we need to migrate to the new format
488
486
  # if there is a database in that location
@@ -498,10 +496,14 @@ class Settings(BaseSettings):
498
496
 
499
497
  if info.data["save_db_in_config_dir"]:
500
498
  database_dir = info.data["config_dir"]
501
- logger.debug(f"Saving database to config_dir: {database_dir}")
502
499
  else:
503
- database_dir = Path(__file__).parent.parent.parent.resolve()
504
- logger.debug(f"Saving database to langflow directory: {database_dir}")
500
+ # Use langflow package path, not lfx, for backwards compatibility
501
+ try:
502
+ import langflow
503
+
504
+ database_dir = Path(langflow.__file__).parent.resolve()
505
+ except ImportError:
506
+ database_dir = Path(__file__).parent.parent.parent.resolve()
505
507
 
506
508
  pre_db_file_name = "langflow-pre.db"
507
509
  db_file_name = "langflow.db"
@@ -524,7 +526,6 @@ class Settings(BaseSettings):
524
526
  logger.debug(f"Creating new database at {new_pre_path}")
525
527
  final_path = new_pre_path
526
528
  elif Path(new_path).exists():
527
- logger.debug(f"Database already exists at {new_path}, using it")
528
529
  final_path = new_path
529
530
  elif Path(f"./{db_file_name}").exists():
530
531
  try:
@@ -568,15 +569,10 @@ class Settings(BaseSettings):
568
569
 
569
570
  if not value:
570
571
  value = [BASE_COMPONENTS_PATH]
571
- logger.debug("Setting default components path to components_path")
572
- else:
573
- if isinstance(value, Path):
574
- value = [str(value)]
575
- elif isinstance(value, list):
576
- value = [str(p) if isinstance(p, Path) else p for p in value]
577
- logger.debug("Adding default components path to components_path")
578
-
579
- logger.debug(f"Components path: {value}")
572
+ elif isinstance(value, Path):
573
+ value = [str(value)]
574
+ elif isinstance(value, list):
575
+ value = [str(p) if isinstance(p, Path) else p for p in value]
580
576
  return value
581
577
 
582
578
  model_config = SettingsConfigDict(validate_assignment=True, extra="ignore", env_prefix="LANGFLOW_")
@@ -587,13 +583,10 @@ class Settings(BaseSettings):
587
583
  self.dev = dev
588
584
 
589
585
  def update_settings(self, **kwargs) -> None:
590
- logger.debug("Updating settings")
591
586
  for key, value in kwargs.items():
592
587
  # value may contain sensitive information, so we don't want to log it
593
588
  if not hasattr(self, key):
594
- logger.debug(f"Key {key} not found in settings")
595
589
  continue
596
- logger.debug(f"Updating {key}")
597
590
  if isinstance(getattr(self, key), list):
598
591
  # value might be a '[something]' string
599
592
  value_ = value
@@ -604,17 +597,12 @@ class Settings(BaseSettings):
604
597
  item_ = str(item) if isinstance(item, Path) else item
605
598
  if item_ not in getattr(self, key):
606
599
  getattr(self, key).append(item_)
607
- logger.debug(f"Extended {key}")
608
600
  else:
609
601
  value_ = str(value_) if isinstance(value_, Path) else value_
610
602
  if value_ not in getattr(self, key):
611
603
  getattr(self, key).append(value_)
612
- logger.debug(f"Appended {key}")
613
-
614
604
  else:
615
605
  setattr(self, key, value)
616
- logger.debug(f"Updated {key}")
617
- logger.debug(f"{key}: {getattr(self, key)}")
618
606
 
619
607
  @property
620
608
  def voice_mode_available(self) -> bool:
@@ -41,3 +41,5 @@ AGENTIC_VARIABLES = [
41
41
  "FIELD_NAME",
42
42
  "ASTRA_TOKEN",
43
43
  ]
44
+
45
+ DEFAULT_AGENTIC_VARIABLE_VALUE = ""
@@ -57,6 +57,43 @@ class LocalStorageService(StorageService):
57
57
  """Build the full path of a file in the local storage."""
58
58
  return str(self.data_dir / flow_id / file_name)
59
59
 
60
+ def parse_file_path(self, full_path: str) -> tuple[str, str]:
61
+ r"""Parse a full local storage path to extract flow_id and file_name.
62
+
63
+ Args:
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". On Windows the
66
+ separators may be backslashes ("\\"). This method handles both.
67
+
68
+ Returns:
69
+ tuple[str, str]: A tuple of (flow_id, file_name)
70
+
71
+ Examples:
72
+ >>> parse_file_path("/data/user_123/image.png") # with data_dir
73
+ ("user_123", "image.png")
74
+ >>> parse_file_path("user_123/image.png") # without data_dir
75
+ ("user_123", "image.png")
76
+ """
77
+ data_dir_str = str(self.data_dir)
78
+
79
+ # Remove data_dir if present (but don't require it)
80
+ path_without_prefix = full_path
81
+ if full_path.startswith(data_dir_str):
82
+ # Strip both POSIX and Windows separators
83
+ path_without_prefix = full_path[len(data_dir_str) :].lstrip("/").lstrip("\\")
84
+
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
92
+
93
+ # Use rsplit to split from the right, limiting to 1 split
94
+ flow_id, file_name = normalized_path.rsplit("/", 1)
95
+ return flow_id, file_name
96
+
60
97
  async def save_file(self, flow_id: str, file_name: str, data: bytes, *, append: bool = False) -> None:
61
98
  """Save a file in the local storage.
62
99
 
@@ -37,6 +37,7 @@ class StorageService(Service):
37
37
  self.data_dir: anyio.Path = anyio.Path(settings_service.settings.config_dir)
38
38
  self.set_ready()
39
39
 
40
+ @abstractmethod
40
41
  def build_full_path(self, flow_id: str, file_name: str) -> str:
41
42
  """Build the full path/key for a file.
42
43
 
@@ -49,6 +50,24 @@ class StorageService(Service):
49
50
  """
50
51
  raise NotImplementedError
51
52
 
53
+ @abstractmethod
54
+ def parse_file_path(self, full_path: str) -> tuple[str, str]:
55
+ """Parse a full storage path to extract flow_id and file_name.
56
+
57
+ This reverses the build_full_path operation.
58
+
59
+ Args:
60
+ full_path: Full path as returned by build_full_path
61
+
62
+ Returns:
63
+ tuple[str, str]: A tuple of (flow_id, file_name)
64
+
65
+ Raises:
66
+ ValueError: If the path format is invalid or doesn't match expected structure
67
+ """
68
+ raise NotImplementedError
69
+
70
+ @abstractmethod
52
71
  def resolve_component_path(self, logical_path: str) -> str:
53
72
  """Convert a logical path to a format that components can use directly.
54
73
 
lfx/utils/constants.py CHANGED
@@ -82,6 +82,7 @@ DIRECT_TYPES = [
82
82
  "query",
83
83
  "tools",
84
84
  "mcp",
85
+ "model",
85
86
  ]
86
87
 
87
88
 
lfx/utils/image.py CHANGED
@@ -6,14 +6,19 @@ import base64
6
6
  from functools import lru_cache
7
7
  from pathlib import Path
8
8
 
9
+ from lfx.log import logger
10
+ from lfx.services.deps import get_storage_service
11
+ from lfx.utils.async_helpers import run_until_complete
9
12
  from lfx.utils.helpers import get_mime_type
10
13
 
11
14
 
12
15
  def convert_image_to_base64(image_path: str | Path) -> str:
13
16
  """Convert an image file to a base64 encoded string.
14
17
 
18
+ Handles both local files and S3 storage paths.
19
+
15
20
  Args:
16
- image_path: Path to the image file
21
+ image_path: Path to the image file (local or S3 path like "flow_id/filename")
17
22
 
18
23
  Returns:
19
24
  Base64 encoded string of the image
@@ -22,6 +27,20 @@ def convert_image_to_base64(image_path: str | Path) -> str:
22
27
  FileNotFoundError: If the image file doesn't exist
23
28
  """
24
29
  image_path = Path(image_path)
30
+
31
+ storage_service = get_storage_service()
32
+ if storage_service:
33
+ flow_id, file_name = storage_service.parse_file_path(str(image_path))
34
+ try:
35
+ file_content = run_until_complete(
36
+ storage_service.get_file(flow_id=flow_id, file_name=file_name) # type: ignore[call-arg]
37
+ )
38
+ return base64.b64encode(file_content).decode("utf-8")
39
+ except Exception as e:
40
+ logger.error(f"Error reading image file: {e}")
41
+ raise
42
+
43
+ # Fall back to local file access
25
44
  if not image_path.exists():
26
45
  msg = f"Image file not found: {image_path}"
27
46
  raise FileNotFoundError(msg)
@@ -34,7 +53,7 @@ def create_data_url(image_path: str | Path, mime_type: str | None = None) -> str
34
53
  """Create a data URL from an image file.
35
54
 
36
55
  Args:
37
- image_path: Path to the image file
56
+ image_path: Path to the image file (local or S3 path like "flow_id/filename")
38
57
  mime_type: MIME type of the image. If None, will be auto-detected
39
58
 
40
59
  Returns:
@@ -44,9 +63,6 @@ def create_data_url(image_path: str | Path, mime_type: str | None = None) -> str
44
63
  FileNotFoundError: If the image file doesn't exist
45
64
  """
46
65
  image_path = Path(image_path)
47
- if not image_path.exists():
48
- msg = f"Image file not found: {image_path}"
49
- raise FileNotFoundError(msg)
50
66
 
51
67
  if mime_type is None:
52
68
  mime_type = get_mime_type(image_path)
@@ -57,14 +73,16 @@ def create_data_url(image_path: str | Path, mime_type: str | None = None) -> str
57
73
 
58
74
  @lru_cache(maxsize=50)
59
75
  def create_image_content_dict(
60
- image_path: str | Path, mime_type: str | None = None, model_name: str | None = None
76
+ image_path: str | Path,
77
+ mime_type: str | None = None,
78
+ model_name: str | None = None, # noqa: ARG001
61
79
  ) -> dict:
62
80
  """Create a content dictionary for multimodal inputs from an image file.
63
81
 
64
82
  Args:
65
- image_path: Path to the image file
83
+ image_path: Path to the image file (local or S3 path like "flow_id/filename")
66
84
  mime_type: MIME type of the image. If None, will be auto-detected
67
- model_name: Optional model parameter to determine content dict structure
85
+ model_name: Optional model parameter (kept for backward compatibility, no longer used)
68
86
 
69
87
  Returns:
70
88
  Content dictionary with type and image_url fields
@@ -74,6 +92,6 @@ def create_image_content_dict(
74
92
  """
75
93
  data_url = create_data_url(image_path, mime_type)
76
94
 
77
- if model_name == "OllamaModel":
78
- return {"type": "image_url", "source_type": "url", "image_url": data_url}
79
- return {"type": "image", "source_type": "url", "url": data_url}
95
+ # Standard format for OpenAI, Anthropic, Gemini, and most providers
96
+ # Format: {"type": "image_url", "image_url": {"url": "data:..."}}
97
+ return {"type": "image_url", "image_url": {"url": data_url}}