lfx-nightly 0.2.0.dev0__py3-none-any.whl → 0.2.0.dev41__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 (196) hide show
  1. lfx/_assets/component_index.json +1 -1
  2. lfx/base/agents/agent.py +21 -4
  3. lfx/base/agents/altk_base_agent.py +393 -0
  4. lfx/base/agents/altk_tool_wrappers.py +565 -0
  5. lfx/base/agents/events.py +2 -1
  6. lfx/base/composio/composio_base.py +159 -224
  7. lfx/base/data/base_file.py +97 -20
  8. lfx/base/data/docling_utils.py +61 -10
  9. lfx/base/data/storage_utils.py +301 -0
  10. lfx/base/data/utils.py +178 -14
  11. lfx/base/mcp/util.py +2 -2
  12. lfx/base/models/anthropic_constants.py +21 -12
  13. lfx/base/models/groq_constants.py +74 -58
  14. lfx/base/models/groq_model_discovery.py +265 -0
  15. lfx/base/models/model.py +1 -1
  16. lfx/base/models/model_utils.py +100 -0
  17. lfx/base/models/openai_constants.py +7 -0
  18. lfx/base/models/watsonx_constants.py +32 -8
  19. lfx/base/tools/run_flow.py +601 -129
  20. lfx/cli/commands.py +9 -4
  21. lfx/cli/common.py +2 -2
  22. lfx/cli/run.py +1 -1
  23. lfx/cli/script_loader.py +53 -11
  24. lfx/components/Notion/create_page.py +1 -1
  25. lfx/components/Notion/list_database_properties.py +1 -1
  26. lfx/components/Notion/list_pages.py +1 -1
  27. lfx/components/Notion/list_users.py +1 -1
  28. lfx/components/Notion/page_content_viewer.py +1 -1
  29. lfx/components/Notion/search.py +1 -1
  30. lfx/components/Notion/update_page_property.py +1 -1
  31. lfx/components/__init__.py +19 -5
  32. lfx/components/{agents → altk}/__init__.py +5 -9
  33. lfx/components/altk/altk_agent.py +193 -0
  34. lfx/components/apify/apify_actor.py +1 -1
  35. lfx/components/composio/__init__.py +70 -18
  36. lfx/components/composio/apollo_composio.py +11 -0
  37. lfx/components/composio/bitbucket_composio.py +11 -0
  38. lfx/components/composio/canva_composio.py +11 -0
  39. lfx/components/composio/coda_composio.py +11 -0
  40. lfx/components/composio/composio_api.py +10 -0
  41. lfx/components/composio/discord_composio.py +1 -1
  42. lfx/components/composio/elevenlabs_composio.py +11 -0
  43. lfx/components/composio/exa_composio.py +11 -0
  44. lfx/components/composio/firecrawl_composio.py +11 -0
  45. lfx/components/composio/fireflies_composio.py +11 -0
  46. lfx/components/composio/gmail_composio.py +1 -1
  47. lfx/components/composio/googlebigquery_composio.py +11 -0
  48. lfx/components/composio/googlecalendar_composio.py +1 -1
  49. lfx/components/composio/googledocs_composio.py +1 -1
  50. lfx/components/composio/googlemeet_composio.py +1 -1
  51. lfx/components/composio/googlesheets_composio.py +1 -1
  52. lfx/components/composio/googletasks_composio.py +1 -1
  53. lfx/components/composio/heygen_composio.py +11 -0
  54. lfx/components/composio/mem0_composio.py +11 -0
  55. lfx/components/composio/peopledatalabs_composio.py +11 -0
  56. lfx/components/composio/perplexityai_composio.py +11 -0
  57. lfx/components/composio/serpapi_composio.py +11 -0
  58. lfx/components/composio/slack_composio.py +3 -574
  59. lfx/components/composio/slackbot_composio.py +1 -1
  60. lfx/components/composio/snowflake_composio.py +11 -0
  61. lfx/components/composio/tavily_composio.py +11 -0
  62. lfx/components/composio/youtube_composio.py +2 -2
  63. lfx/components/cuga/__init__.py +34 -0
  64. lfx/components/cuga/cuga_agent.py +730 -0
  65. lfx/components/data/__init__.py +78 -28
  66. lfx/components/data_source/__init__.py +58 -0
  67. lfx/components/{data → data_source}/api_request.py +26 -3
  68. lfx/components/{data → data_source}/csv_to_data.py +15 -10
  69. lfx/components/{data → data_source}/json_to_data.py +15 -8
  70. lfx/components/{data → data_source}/news_search.py +1 -1
  71. lfx/components/{data → data_source}/rss.py +1 -1
  72. lfx/components/{data → data_source}/sql_executor.py +1 -1
  73. lfx/components/{data → data_source}/url.py +1 -1
  74. lfx/components/{data → data_source}/web_search.py +1 -1
  75. lfx/components/datastax/astradb_cql.py +1 -1
  76. lfx/components/datastax/astradb_graph.py +1 -1
  77. lfx/components/datastax/astradb_tool.py +1 -1
  78. lfx/components/datastax/astradb_vectorstore.py +1 -1
  79. lfx/components/datastax/hcd.py +1 -1
  80. lfx/components/deactivated/json_document_builder.py +1 -1
  81. lfx/components/docling/__init__.py +0 -3
  82. lfx/components/docling/chunk_docling_document.py +3 -1
  83. lfx/components/docling/export_docling_document.py +3 -1
  84. lfx/components/elastic/elasticsearch.py +1 -1
  85. lfx/components/files_and_knowledge/__init__.py +47 -0
  86. lfx/components/{data → files_and_knowledge}/directory.py +1 -1
  87. lfx/components/{data → files_and_knowledge}/file.py +304 -24
  88. lfx/components/{knowledge_bases → files_and_knowledge}/retrieval.py +2 -2
  89. lfx/components/{data → files_and_knowledge}/save_file.py +218 -31
  90. lfx/components/flow_controls/__init__.py +58 -0
  91. lfx/components/{logic → flow_controls}/conditional_router.py +1 -1
  92. lfx/components/{logic → flow_controls}/loop.py +43 -9
  93. lfx/components/flow_controls/run_flow.py +108 -0
  94. lfx/components/glean/glean_search_api.py +1 -1
  95. lfx/components/groq/groq.py +35 -28
  96. lfx/components/helpers/__init__.py +102 -0
  97. lfx/components/ibm/watsonx.py +7 -1
  98. lfx/components/input_output/__init__.py +3 -1
  99. lfx/components/input_output/chat.py +4 -3
  100. lfx/components/input_output/chat_output.py +10 -4
  101. lfx/components/input_output/text.py +1 -1
  102. lfx/components/input_output/text_output.py +1 -1
  103. lfx/components/{data → input_output}/webhook.py +1 -1
  104. lfx/components/knowledge_bases/__init__.py +59 -4
  105. lfx/components/langchain_utilities/character.py +1 -1
  106. lfx/components/langchain_utilities/csv_agent.py +84 -16
  107. lfx/components/langchain_utilities/json_agent.py +67 -12
  108. lfx/components/langchain_utilities/language_recursive.py +1 -1
  109. lfx/components/llm_operations/__init__.py +46 -0
  110. lfx/components/{processing → llm_operations}/batch_run.py +17 -8
  111. lfx/components/{processing → llm_operations}/lambda_filter.py +1 -1
  112. lfx/components/{logic → llm_operations}/llm_conditional_router.py +1 -1
  113. lfx/components/{processing/llm_router.py → llm_operations/llm_selector.py} +3 -3
  114. lfx/components/{processing → llm_operations}/structured_output.py +1 -1
  115. lfx/components/logic/__init__.py +126 -0
  116. lfx/components/mem0/mem0_chat_memory.py +11 -0
  117. lfx/components/models/__init__.py +64 -9
  118. lfx/components/models_and_agents/__init__.py +49 -0
  119. lfx/components/{agents → models_and_agents}/agent.py +6 -4
  120. lfx/components/models_and_agents/embedding_model.py +353 -0
  121. lfx/components/models_and_agents/language_model.py +398 -0
  122. lfx/components/{agents → models_and_agents}/mcp_component.py +53 -44
  123. lfx/components/{helpers → models_and_agents}/memory.py +1 -1
  124. lfx/components/nvidia/system_assist.py +1 -1
  125. lfx/components/olivya/olivya.py +1 -1
  126. lfx/components/ollama/ollama.py +24 -5
  127. lfx/components/processing/__init__.py +9 -60
  128. lfx/components/processing/converter.py +1 -1
  129. lfx/components/processing/dataframe_operations.py +1 -1
  130. lfx/components/processing/parse_json_data.py +2 -2
  131. lfx/components/processing/parser.py +1 -1
  132. lfx/components/processing/split_text.py +1 -1
  133. lfx/components/qdrant/qdrant.py +1 -1
  134. lfx/components/redis/redis.py +1 -1
  135. lfx/components/twelvelabs/split_video.py +10 -0
  136. lfx/components/twelvelabs/video_file.py +12 -0
  137. lfx/components/utilities/__init__.py +43 -0
  138. lfx/components/{helpers → utilities}/calculator_core.py +1 -1
  139. lfx/components/{helpers → utilities}/current_date.py +1 -1
  140. lfx/components/{processing → utilities}/python_repl_core.py +1 -1
  141. lfx/components/vectorstores/local_db.py +9 -0
  142. lfx/components/youtube/youtube_transcripts.py +118 -30
  143. lfx/custom/custom_component/component.py +57 -1
  144. lfx/custom/custom_component/custom_component.py +68 -6
  145. lfx/custom/directory_reader/directory_reader.py +5 -2
  146. lfx/graph/edge/base.py +43 -20
  147. lfx/graph/state/model.py +15 -2
  148. lfx/graph/utils.py +6 -0
  149. lfx/graph/vertex/param_handler.py +10 -7
  150. lfx/helpers/__init__.py +12 -0
  151. lfx/helpers/flow.py +117 -0
  152. lfx/inputs/input_mixin.py +24 -1
  153. lfx/inputs/inputs.py +13 -1
  154. lfx/interface/components.py +161 -83
  155. lfx/log/logger.py +5 -3
  156. lfx/schema/image.py +2 -12
  157. lfx/services/database/__init__.py +5 -0
  158. lfx/services/database/service.py +25 -0
  159. lfx/services/deps.py +87 -22
  160. lfx/services/interfaces.py +5 -0
  161. lfx/services/manager.py +24 -10
  162. lfx/services/mcp_composer/service.py +1029 -162
  163. lfx/services/session.py +5 -0
  164. lfx/services/settings/auth.py +18 -11
  165. lfx/services/settings/base.py +56 -30
  166. lfx/services/settings/constants.py +8 -0
  167. lfx/services/storage/local.py +108 -46
  168. lfx/services/storage/service.py +171 -29
  169. lfx/template/field/base.py +3 -0
  170. lfx/utils/image.py +29 -11
  171. lfx/utils/ssrf_protection.py +384 -0
  172. lfx/utils/validate_cloud.py +26 -0
  173. {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev41.dist-info}/METADATA +38 -22
  174. {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev41.dist-info}/RECORD +189 -160
  175. {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev41.dist-info}/WHEEL +1 -1
  176. lfx/components/agents/altk_agent.py +0 -366
  177. lfx/components/agents/cuga_agent.py +0 -1013
  178. lfx/components/docling/docling_remote_vlm.py +0 -284
  179. lfx/components/logic/run_flow.py +0 -71
  180. lfx/components/models/embedding_model.py +0 -195
  181. lfx/components/models/language_model.py +0 -144
  182. lfx/components/processing/dataframe_to_toolset.py +0 -259
  183. /lfx/components/{data → data_source}/mock_data.py +0 -0
  184. /lfx/components/{knowledge_bases → files_and_knowledge}/ingestion.py +0 -0
  185. /lfx/components/{logic → flow_controls}/data_conditional_router.py +0 -0
  186. /lfx/components/{logic → flow_controls}/flow_tool.py +0 -0
  187. /lfx/components/{logic → flow_controls}/listen.py +0 -0
  188. /lfx/components/{logic → flow_controls}/notify.py +0 -0
  189. /lfx/components/{logic → flow_controls}/pass_message.py +0 -0
  190. /lfx/components/{logic → flow_controls}/sub_flow.py +0 -0
  191. /lfx/components/{processing → models_and_agents}/prompt.py +0 -0
  192. /lfx/components/{helpers → processing}/create_list.py +0 -0
  193. /lfx/components/{helpers → processing}/output_parser.py +0 -0
  194. /lfx/components/{helpers → processing}/store_message.py +0 -0
  195. /lfx/components/{helpers → utilities}/id_generator.py +0 -0
  196. {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev41.dist-info}/entry_points.txt +0 -0
lfx/services/session.py CHANGED
@@ -75,6 +75,11 @@ class NoopSession:
75
75
  """Context manager that disables autoflush (no-op implementation)."""
76
76
  return self
77
77
 
78
+ @property
79
+ def is_active(self):
80
+ """Check if session is active (always True for NoopSession)."""
81
+ return True
82
+
78
83
  def __enter__(self):
79
84
  return self
80
85
 
@@ -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()
@@ -17,7 +17,7 @@ from typing_extensions import override
17
17
  from lfx.constants import BASE_COMPONENTS_PATH
18
18
  from lfx.log.logger import logger
19
19
  from lfx.serialization.constants import MAX_ITEMS_LENGTH, MAX_TEXT_LENGTH
20
- from lfx.services.settings.constants import VARIABLES_TO_GET_FROM_ENVIRONMENT
20
+ from lfx.services.settings.constants import AGENTIC_VARIABLES, VARIABLES_TO_GET_FROM_ENVIRONMENT
21
21
  from lfx.utils.util_strings import is_valid_database_url
22
22
 
23
23
 
@@ -87,6 +87,10 @@ class Settings(BaseSettings):
87
87
  db_connect_timeout: int = 30
88
88
  """The number of seconds to wait before giving up on a lock to released or establishing a connection to the
89
89
  database."""
90
+ migration_lock_namespace: str | None = None
91
+ """Optional namespace identifier for PostgreSQL advisory lock during migrations.
92
+ If not provided, a hash of the database URL will be used. Useful when multiple Langflow
93
+ instances share the same database and need coordinated migration locking."""
90
94
 
91
95
  mcp_server_timeout: int = 20
92
96
  """The number of seconds to wait before giving up on a lock to released or establishing a connection to the
@@ -109,7 +113,7 @@ class Settings(BaseSettings):
109
113
  reap idle sessions."""
110
114
 
111
115
  # sqlite configuration
112
- sqlite_pragmas: dict | None = {"synchronous": "NORMAL", "journal_mode": "WAL"}
116
+ sqlite_pragmas: dict | None = {"synchronous": "NORMAL", "journal_mode": "WAL", "busy_timeout": 30000}
113
117
  """SQLite pragmas to use when connecting to the database."""
114
118
 
115
119
  db_driver_connection_settings: dict | None = None
@@ -187,6 +191,13 @@ class Settings(BaseSettings):
187
191
  like_webhook_url: str | None = "https://api.langflow.store/flows/trigger/64275852-ec00-45c1-984e-3bff814732da"
188
192
 
189
193
  storage_type: str = "local"
194
+ """Storage type for file storage. Defaults to 'local'. Supports 'local' and 's3'."""
195
+ object_storage_bucket_name: str | None = "langflow-bucket"
196
+ """Object storage bucket name for file storage. Defaults to 'langflow-bucket'."""
197
+ object_storage_prefix: str | None = "files"
198
+ """Object storage prefix for file storage. Defaults to 'files'."""
199
+ object_storage_tags: dict[str, str] | None = None
200
+ """Object storage tags for file storage."""
190
201
 
191
202
  celery_enabled: bool = False
192
203
 
@@ -245,6 +256,8 @@ class Settings(BaseSettings):
245
256
  """The path to log file for Langflow."""
246
257
  alembic_log_file: str = "alembic/alembic.log"
247
258
  """The path to log file for Alembic for SQLAlchemy."""
259
+ alembic_log_to_stdout: bool = False
260
+ """If set to True, the log file will be ignored and Alembic will log to stdout."""
248
261
  frontend_path: str | None = None
249
262
  """The path to the frontend directory containing build files. This is for development purposes only.."""
250
263
  open_browser: bool = False
@@ -292,9 +305,13 @@ class Settings(BaseSettings):
292
305
  # MCP Composer
293
306
  mcp_composer_enabled: bool = True
294
307
  """If set to False, Langflow will not start the MCP Composer service."""
295
- mcp_composer_version: str = "~=0.1.0.7"
296
- """Version constraint for mcp-composer when using uvx. Uses PEP 440 syntax.
297
- ~=0.1.0.7 allows patch updates (0.1.0.x) but prevents minor/major version changes."""
308
+ mcp_composer_version: str = "==0.1.0.8.10"
309
+ """Version constraint for mcp-composer when using uvx. Uses PEP 440 syntax."""
310
+
311
+ # Agentic Experience
312
+ agentic_experience: bool = False
313
+ """If set to True, Langflow will start the agentic MCP server that provides tools for
314
+ flow/component operations, template search, and graph visualization."""
298
315
 
299
316
  # Public Flow Settings
300
317
  public_flow_cleanup_interval: int = Field(default=3600, gt=600)
@@ -317,6 +334,23 @@ class Settings(BaseSettings):
317
334
  update_starter_projects: bool = True
318
335
  """If set to True, Langflow will update starter projects."""
319
336
 
337
+ # SSRF Protection
338
+ ssrf_protection_enabled: bool = False
339
+ """If set to True, Langflow will enable SSRF (Server-Side Request Forgery) protection.
340
+ When enabled, blocks requests to private IP ranges, localhost, and cloud metadata endpoints.
341
+ When False (default), no URL validation is performed, allowing requests to any destination
342
+ including internal services, private networks, and cloud metadata endpoints.
343
+ Default is False for backward compatibility. In v2.0, this will be changed to True.
344
+
345
+ Note: When ssrf_protection_enabled is disabled, the ssrf_allowed_hosts setting is ignored and has no effect."""
346
+ ssrf_allowed_hosts: list[str] = []
347
+ """Comma-separated list of hosts/IPs/CIDR ranges to allow despite SSRF protection.
348
+ Examples: 'internal-api.company.local,192.168.1.0/24,10.0.0.5,*.dev.internal'
349
+ Supports exact hostnames, wildcard domains (*.example.com), exact IPs, and CIDR ranges.
350
+
351
+ Note: This setting only takes effect when ssrf_protection_enabled is True.
352
+ When protection is disabled, all hosts are allowed regardless of this setting."""
353
+
320
354
  @field_validator("cors_origins", mode="before")
321
355
  @classmethod
322
356
  def validate_cors_origins(cls, value):
@@ -367,7 +401,7 @@ class Settings(BaseSettings):
367
401
  Supports PEP 440 specifiers: ==, !=, <=, >=, <, >, ~=, ===
368
402
  """
369
403
  if not value:
370
- return "~=0.1.0.7" # Default
404
+ return "==0.1.0.8.10" # Default
371
405
 
372
406
  # Check if it already has a version specifier
373
407
  # Order matters: check longer specifiers first to avoid false matches
@@ -389,9 +423,19 @@ class Settings(BaseSettings):
389
423
  @field_validator("variables_to_get_from_environment", mode="before")
390
424
  @classmethod
391
425
  def set_variables_to_get_from_environment(cls, value):
426
+ import os
427
+
392
428
  if isinstance(value, str):
393
429
  value = value.split(",")
394
- return list(set(VARIABLES_TO_GET_FROM_ENVIRONMENT + value))
430
+
431
+ result = list(set(VARIABLES_TO_GET_FROM_ENVIRONMENT + value))
432
+
433
+ # Add agentic variables if agentic_experience is enabled
434
+ # Check env var directly since we can't access instance attributes in validator
435
+ if os.getenv("LANGFLOW_AGENTIC_EXPERIENCE", "true").lower() == "true":
436
+ result.extend(AGENTIC_VARIABLES)
437
+
438
+ return list(set(result))
395
439
 
396
440
  @field_validator("log_file", mode="before")
397
441
  @classmethod
@@ -433,12 +477,10 @@ class Settings(BaseSettings):
433
477
  msg = f"Invalid database_url provided: '{value}'"
434
478
  raise ValueError(msg)
435
479
 
436
- logger.debug("No database_url provided, trying LANGFLOW_DATABASE_URL env variable")
437
480
  if langflow_database_url := os.getenv("LANGFLOW_DATABASE_URL"):
438
481
  value = langflow_database_url
439
- logger.debug("Using LANGFLOW_DATABASE_URL env variable.")
482
+ logger.debug("Using LANGFLOW_DATABASE_URL env variable")
440
483
  else:
441
- logger.debug("No database_url env variable, using sqlite database")
442
484
  # Originally, we used sqlite:///./langflow.db
443
485
  # so we need to migrate to the new format
444
486
  # if there is a database in that location
@@ -454,10 +496,8 @@ class Settings(BaseSettings):
454
496
 
455
497
  if info.data["save_db_in_config_dir"]:
456
498
  database_dir = info.data["config_dir"]
457
- logger.debug(f"Saving database to config_dir: {database_dir}")
458
499
  else:
459
500
  database_dir = Path(__file__).parent.parent.parent.resolve()
460
- logger.debug(f"Saving database to langflow directory: {database_dir}")
461
501
 
462
502
  pre_db_file_name = "langflow-pre.db"
463
503
  db_file_name = "langflow.db"
@@ -480,7 +520,6 @@ class Settings(BaseSettings):
480
520
  logger.debug(f"Creating new database at {new_pre_path}")
481
521
  final_path = new_pre_path
482
522
  elif Path(new_path).exists():
483
- logger.debug(f"Database already exists at {new_path}, using it")
484
523
  final_path = new_path
485
524
  elif Path(f"./{db_file_name}").exists():
486
525
  try:
@@ -524,15 +563,10 @@ class Settings(BaseSettings):
524
563
 
525
564
  if not value:
526
565
  value = [BASE_COMPONENTS_PATH]
527
- logger.debug("Setting default components path to components_path")
528
- else:
529
- if isinstance(value, Path):
530
- value = [str(value)]
531
- elif isinstance(value, list):
532
- value = [str(p) if isinstance(p, Path) else p for p in value]
533
- logger.debug("Adding default components path to components_path")
534
-
535
- logger.debug(f"Components path: {value}")
566
+ elif isinstance(value, Path):
567
+ value = [str(value)]
568
+ elif isinstance(value, list):
569
+ value = [str(p) if isinstance(p, Path) else p for p in value]
536
570
  return value
537
571
 
538
572
  model_config = SettingsConfigDict(validate_assignment=True, extra="ignore", env_prefix="LANGFLOW_")
@@ -543,13 +577,10 @@ class Settings(BaseSettings):
543
577
  self.dev = dev
544
578
 
545
579
  def update_settings(self, **kwargs) -> None:
546
- logger.debug("Updating settings")
547
580
  for key, value in kwargs.items():
548
581
  # value may contain sensitive information, so we don't want to log it
549
582
  if not hasattr(self, key):
550
- logger.debug(f"Key {key} not found in settings")
551
583
  continue
552
- logger.debug(f"Updating {key}")
553
584
  if isinstance(getattr(self, key), list):
554
585
  # value might be a '[something]' string
555
586
  value_ = value
@@ -560,17 +591,12 @@ class Settings(BaseSettings):
560
591
  item_ = str(item) if isinstance(item, Path) else item
561
592
  if item_ not in getattr(self, key):
562
593
  getattr(self, key).append(item_)
563
- logger.debug(f"Extended {key}")
564
594
  else:
565
595
  value_ = str(value_) if isinstance(value_, Path) else value_
566
596
  if value_ not in getattr(self, key):
567
597
  getattr(self, key).append(value_)
568
- logger.debug(f"Appended {key}")
569
-
570
598
  else:
571
599
  setattr(self, key, value)
572
- logger.debug(f"Updated {key}")
573
- logger.debug(f"{key}: {getattr(self, key)}")
574
600
 
575
601
  @property
576
602
  def voice_mode_available(self) -> bool:
@@ -33,3 +33,11 @@ VARIABLES_TO_GET_FROM_ENVIRONMENT = [
33
33
  "TAVILY_API_KEY",
34
34
  "COMETAPI_KEY",
35
35
  ]
36
+
37
+ # Agentic experience specific variables
38
+ AGENTIC_VARIABLES = [
39
+ "FLOW_ID",
40
+ "COMPONENT_ID",
41
+ "FIELD_NAME",
42
+ "ASTRA_TOKEN",
43
+ ]
@@ -1,30 +1,102 @@
1
1
  """Local file-based storage service for lfx package."""
2
2
 
3
- from pathlib import Path
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from aiofile import async_open
4
8
 
5
9
  from lfx.log.logger import logger
6
10
  from lfx.services.storage.service import StorageService
7
11
 
12
+ if TYPE_CHECKING:
13
+ from langflow.services.session.service import SessionService
14
+
15
+ from lfx.services.settings.service import SettingsService
16
+
17
+ # Constants for path parsing
18
+ EXPECTED_PATH_PARTS = 2 # Path format: "flow_id/filename"
19
+
8
20
 
9
21
  class LocalStorageService(StorageService):
10
22
  """A service class for handling local file storage operations."""
11
23
 
12
- def __init__(self, data_dir: str | Path | None = None) -> None:
13
- """Initialize the local storage service."""
14
- super().__init__(data_dir)
15
- self.set_ready()
24
+ def __init__(
25
+ self,
26
+ session_service: SessionService,
27
+ settings_service: SettingsService,
28
+ ) -> None:
29
+ """Initialize the local storage service.
30
+
31
+ Args:
32
+ session_service: Session service instance
33
+ settings_service: Settings service instance containing configuration
34
+ """
35
+ # Initialize base class with services
36
+ super().__init__(session_service, settings_service)
37
+ # Base class already sets self.data_dir as anyio.Path from settings_service.settings.config_dir
38
+
39
+ def resolve_component_path(self, logical_path: str) -> str:
40
+ """Convert logical path to absolute filesystem path for local storage.
41
+
42
+ Args:
43
+ logical_path: Path in format "flow_id/filename"
44
+ Returns:
45
+ str: Absolute filesystem path
46
+ """
47
+ # Split the logical path into flow_id and filename
48
+ parts = logical_path.split("/", 1)
49
+ if len(parts) != EXPECTED_PATH_PARTS:
50
+ # Handle edge case - return as-is if format is unexpected
51
+ return logical_path
52
+
53
+ flow_id, file_name = parts
54
+ return self.build_full_path(flow_id, file_name)
16
55
 
17
56
  def build_full_path(self, flow_id: str, file_name: str) -> str:
18
57
  """Build the full path of a file in the local storage."""
19
58
  return str(self.data_dir / flow_id / file_name)
20
59
 
21
- async def save_file(self, flow_id: str, file_name: str, data: bytes) -> None:
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.
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"
66
+
67
+ Returns:
68
+ tuple[str, str]: A tuple of (flow_id, file_name)
69
+
70
+ Examples:
71
+ >>> parse_file_path("/data/user_123/image.png") # with data_dir
72
+ ("user_123", "image.png")
73
+ >>> parse_file_path("user_123/image.png") # without data_dir
74
+ ("user_123", "image.png")
75
+ """
76
+ data_dir_str = str(self.data_dir)
77
+
78
+ # Remove data_dir if present (but don't require it)
79
+ path_without_prefix = full_path
80
+ if full_path.startswith(data_dir_str):
81
+ path_without_prefix = full_path[len(data_dir_str) :].lstrip("/")
82
+
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
87
+
88
+ # Use rsplit to split from the right, limiting to 1 split
89
+ flow_id, file_name = path_without_prefix.rsplit("/", 1)
90
+ return flow_id, file_name
91
+
92
+ async def save_file(self, flow_id: str, file_name: str, data: bytes, *, append: bool = False) -> None:
22
93
  """Save a file in the local storage.
23
94
 
24
95
  Args:
25
96
  flow_id: The identifier for the flow.
26
97
  file_name: The name of the file to be saved.
27
98
  data: The byte content of the file.
99
+ append: If True, append to existing file; if False, overwrite.
28
100
 
29
101
  Raises:
30
102
  FileNotFoundError: If the specified flow does not exist.
@@ -32,17 +104,18 @@ class LocalStorageService(StorageService):
32
104
  PermissionError: If there is no permission to write the file.
33
105
  """
34
106
  folder_path = self.data_dir / flow_id
35
- folder_path.mkdir(parents=True, exist_ok=True)
107
+ await folder_path.mkdir(parents=True, exist_ok=True)
36
108
  file_path = folder_path / file_name
37
109
 
38
110
  try:
39
- with file_path.open("wb") as f:
40
- f.write(data)
111
+ mode = "ab" if append else "wb"
112
+ async with async_open(str(file_path), mode) as f:
113
+ await f.write(data)
114
+ action = "appended to" if append else "saved"
115
+ await logger.ainfo(f"File {file_name} {action} successfully in flow {flow_id}.")
41
116
  except Exception:
42
117
  logger.exception(f"Error saving file {file_name} in flow {flow_id}")
43
118
  raise
44
- else:
45
- logger.info(f"File {file_name} saved successfully in flow {flow_id}.")
46
119
 
47
120
  async def get_file(self, flow_id: str, file_name: str) -> bytes:
48
121
  """Retrieve a file from the local storage.
@@ -58,20 +131,16 @@ class LocalStorageService(StorageService):
58
131
  FileNotFoundError: If the file does not exist.
59
132
  """
60
133
  file_path = self.data_dir / flow_id / file_name
61
- if not file_path.exists():
62
- logger.warning(f"File {file_name} not found in flow {flow_id}.")
134
+ if not await file_path.exists():
135
+ await logger.awarning(f"File {file_name} not found in flow {flow_id}.")
63
136
  msg = f"File {file_name} not found in flow {flow_id}"
64
137
  raise FileNotFoundError(msg)
65
138
 
66
- try:
67
- with file_path.open("rb") as f:
68
- content = f.read()
69
- except Exception:
70
- logger.exception(f"Error reading file {file_name} in flow {flow_id}")
71
- raise
72
- else:
73
- logger.debug(f"File {file_name} retrieved successfully from flow {flow_id}.")
74
- return content
139
+ async with async_open(str(file_path), "rb") as f:
140
+ content = await f.read()
141
+
142
+ logger.debug(f"File {file_name} retrieved successfully from flow {flow_id}.")
143
+ return content
75
144
 
76
145
  async def list_files(self, flow_id: str) -> list[str]:
77
146
  """List all files in a specific flow directory.
@@ -86,21 +155,17 @@ class LocalStorageService(StorageService):
86
155
  flow_id = str(flow_id)
87
156
 
88
157
  folder_path = self.data_dir / flow_id
89
- if not folder_path.exists():
90
- logger.debug(f"Flow folder {flow_id} does not exist.")
91
- return []
92
-
93
- if not folder_path.is_dir():
94
- logger.warning(f"Flow path {flow_id} is not a directory.")
158
+ if not await folder_path.exists() or not await folder_path.is_dir():
159
+ await logger.awarning(f"Flow {flow_id} directory does not exist.")
95
160
  return []
96
161
 
97
162
  try:
98
- files = [item.name for item in folder_path.iterdir() if item.is_file()]
163
+ files = [p.name async for p in folder_path.iterdir() if await p.is_file()]
99
164
  except Exception: # noqa: BLE001
100
165
  logger.exception(f"Error listing files in flow {flow_id}")
101
166
  return []
102
167
  else:
103
- logger.debug(f"Listed {len(files)} files in flow {flow_id}.")
168
+ await logger.ainfo(f"Listed {len(files)} files in flow {flow_id}.")
104
169
  return files
105
170
 
106
171
  async def delete_file(self, flow_id: str, file_name: str) -> None:
@@ -114,17 +179,11 @@ class LocalStorageService(StorageService):
114
179
  FileNotFoundError: If the file does not exist.
115
180
  """
116
181
  file_path = self.data_dir / flow_id / file_name
117
- if not file_path.exists():
118
- logger.warning(f"File {file_name} not found in flow {flow_id}.")
119
- msg = f"File {file_name} not found in flow {flow_id}"
120
- raise FileNotFoundError(msg)
121
-
122
- try:
123
- file_path.unlink()
124
- logger.info(f"File {file_name} deleted successfully from flow {flow_id}.")
125
- except Exception:
126
- logger.exception(f"Error deleting file {file_name} in flow {flow_id}")
127
- raise
182
+ if await file_path.exists():
183
+ await file_path.unlink()
184
+ await logger.ainfo(f"File {file_name} deleted successfully from flow {flow_id}.")
185
+ else:
186
+ await logger.awarning(f"Attempted to delete non-existent file {file_name} in flow {flow_id}.")
128
187
 
129
188
  async def get_file_size(self, flow_id: str, file_name: str) -> int:
130
189
  """Get the size of a file in bytes.
@@ -140,16 +199,19 @@ class LocalStorageService(StorageService):
140
199
  FileNotFoundError: If the file does not exist.
141
200
  """
142
201
  file_path = self.data_dir / flow_id / file_name
143
- if not file_path.exists():
144
- logger.warning(f"File {file_name} not found in flow {flow_id}.")
202
+ if not await file_path.exists():
203
+ await logger.awarning(f"File {file_name} not found in flow {flow_id}.")
145
204
  msg = f"File {file_name} not found in flow {flow_id}"
146
205
  raise FileNotFoundError(msg)
147
206
 
148
207
  try:
149
- size = file_path.stat().st_size
208
+ file_size_stat = await file_path.stat()
150
209
  except Exception:
151
210
  logger.exception(f"Error getting size of file {file_name} in flow {flow_id}")
152
211
  raise
153
212
  else:
154
- logger.debug(f"File {file_name} size: {size} bytes in flow {flow_id}.")
155
- return size
213
+ return file_size_stat.st_size
214
+
215
+ async def teardown(self) -> None:
216
+ """Perform any cleanup operations when the service is being torn down."""
217
+ # No specific teardown actions required for local