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.
- lfx/_assets/component_index.json +1 -1
- lfx/base/agents/agent.py +9 -4
- lfx/base/agents/altk_base_agent.py +16 -3
- lfx/base/agents/altk_tool_wrappers.py +1 -1
- lfx/base/agents/utils.py +4 -0
- lfx/base/composio/composio_base.py +78 -41
- lfx/base/data/base_file.py +14 -4
- lfx/base/data/cloud_storage_utils.py +156 -0
- lfx/base/data/docling_utils.py +191 -65
- lfx/base/data/storage_utils.py +109 -0
- lfx/base/datastax/astradb_base.py +75 -64
- lfx/base/mcp/util.py +2 -2
- lfx/base/models/__init__.py +11 -1
- lfx/base/models/anthropic_constants.py +21 -12
- lfx/base/models/google_generative_ai_constants.py +33 -9
- lfx/base/models/model_metadata.py +6 -0
- lfx/base/models/ollama_constants.py +196 -30
- lfx/base/models/openai_constants.py +37 -10
- lfx/base/models/unified_models.py +1123 -0
- lfx/base/models/watsonx_constants.py +36 -0
- lfx/base/tools/component_tool.py +2 -9
- lfx/cli/commands.py +6 -1
- lfx/cli/run.py +65 -409
- lfx/cli/script_loader.py +13 -3
- lfx/components/__init__.py +0 -3
- lfx/components/composio/github_composio.py +1 -1
- lfx/components/cuga/cuga_agent.py +39 -27
- lfx/components/data_source/api_request.py +4 -2
- lfx/components/docling/__init__.py +45 -11
- lfx/components/docling/chunk_docling_document.py +3 -1
- lfx/components/docling/docling_inline.py +39 -49
- lfx/components/docling/export_docling_document.py +3 -1
- lfx/components/elastic/opensearch_multimodal.py +215 -57
- lfx/components/files_and_knowledge/file.py +439 -39
- lfx/components/files_and_knowledge/ingestion.py +8 -0
- lfx/components/files_and_knowledge/retrieval.py +10 -0
- lfx/components/files_and_knowledge/save_file.py +123 -53
- lfx/components/ibm/watsonx.py +7 -1
- lfx/components/input_output/chat_output.py +7 -1
- lfx/components/langchain_utilities/tool_calling.py +14 -6
- lfx/components/llm_operations/batch_run.py +80 -25
- lfx/components/llm_operations/lambda_filter.py +33 -6
- lfx/components/llm_operations/llm_conditional_router.py +39 -7
- lfx/components/llm_operations/structured_output.py +38 -12
- lfx/components/models/__init__.py +16 -74
- lfx/components/models_and_agents/agent.py +51 -201
- lfx/components/models_and_agents/embedding_model.py +185 -339
- lfx/components/models_and_agents/language_model.py +54 -318
- lfx/components/models_and_agents/mcp_component.py +58 -9
- lfx/components/ollama/ollama.py +9 -4
- lfx/components/ollama/ollama_embeddings.py +2 -1
- lfx/components/openai/openai_chat_model.py +1 -1
- lfx/components/processing/__init__.py +0 -3
- lfx/components/vllm/__init__.py +37 -0
- lfx/components/vllm/vllm.py +141 -0
- lfx/components/vllm/vllm_embeddings.py +110 -0
- lfx/custom/custom_component/custom_component.py +8 -6
- lfx/custom/directory_reader/directory_reader.py +5 -2
- lfx/graph/utils.py +64 -18
- lfx/inputs/__init__.py +2 -0
- lfx/inputs/input_mixin.py +54 -0
- lfx/inputs/inputs.py +115 -0
- lfx/interface/initialize/loading.py +42 -12
- lfx/io/__init__.py +2 -0
- lfx/run/__init__.py +5 -0
- lfx/run/base.py +494 -0
- lfx/schema/data.py +1 -1
- lfx/schema/image.py +28 -19
- lfx/schema/message.py +19 -3
- lfx/services/interfaces.py +5 -0
- lfx/services/manager.py +5 -4
- lfx/services/mcp_composer/service.py +45 -13
- lfx/services/settings/auth.py +18 -11
- lfx/services/settings/base.py +12 -24
- lfx/services/settings/constants.py +2 -0
- lfx/services/storage/local.py +37 -0
- lfx/services/storage/service.py +19 -0
- lfx/utils/constants.py +1 -0
- lfx/utils/image.py +29 -11
- lfx/utils/validate_cloud.py +14 -3
- {lfx_nightly-0.2.0.dev26.dist-info → lfx_nightly-0.2.1.dev7.dist-info}/METADATA +5 -2
- {lfx_nightly-0.2.0.dev26.dist-info → lfx_nightly-0.2.1.dev7.dist-info}/RECORD +84 -78
- lfx/components/processing/dataframe_to_toolset.py +0 -259
- {lfx_nightly-0.2.0.dev26.dist-info → lfx_nightly-0.2.1.dev7.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
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
|
-
|
|
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"
|
|
305
|
+
if kwargs.get("files"):
|
|
290
306
|
return await asyncio.to_thread(cls, **kwargs)
|
|
291
307
|
return cls(**kwargs)
|
|
292
308
|
|
lfx/services/interfaces.py
CHANGED
|
@@ -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
|
|
159
|
-
|
|
160
|
-
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
1267
|
+
"http",
|
|
1268
|
+
"--endpoint",
|
|
1269
|
+
streamable_http_url,
|
|
1244
1270
|
"--sse-url",
|
|
1245
|
-
|
|
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
|
-
"
|
|
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():
|
lfx/services/settings/auth.py
CHANGED
|
@@ -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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if
|
|
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()
|
lfx/services/settings/base.py
CHANGED
|
@@ -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
|
-
|
|
504
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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:
|
lfx/services/storage/local.py
CHANGED
|
@@ -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
|
|
lfx/services/storage/service.py
CHANGED
|
@@ -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
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,
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
return {"type": "
|
|
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}}
|