lfx-nightly 0.1.12.dev32__py3-none-any.whl → 0.1.12.dev34__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.

Potentially problematic release.


This version of lfx-nightly might be problematic. Click here for more details.

lfx/base/agents/events.py CHANGED
@@ -80,7 +80,7 @@ async def handle_on_chain_start(
80
80
  header={"title": "Input", "icon": "MessageSquare"},
81
81
  )
82
82
  agent_message.content_blocks[0].contents.append(text_content)
83
- agent_message = await send_message_method(message=agent_message)
83
+ agent_message = await send_message_method(message=agent_message, skip_db_update=True)
84
84
  start_time = perf_counter()
85
85
  return agent_message, start_time
86
86
 
@@ -151,7 +151,7 @@ async def handle_on_chain_end(
151
151
  header={"title": "Output", "icon": "MessageSquare"},
152
152
  )
153
153
  agent_message.content_blocks[0].contents.append(text_content)
154
- agent_message = await send_message_method(message=agent_message)
154
+ agent_message = await send_message_method(message=agent_message, skip_db_update=True)
155
155
  start_time = perf_counter()
156
156
  return agent_message, start_time
157
157
 
@@ -190,7 +190,7 @@ async def handle_on_tool_start(
190
190
  tool_blocks_map[tool_key] = tool_content
191
191
  agent_message.content_blocks[0].contents.append(tool_content)
192
192
 
193
- agent_message = await send_message_method(message=agent_message)
193
+ agent_message = await send_message_method(message=agent_message, skip_db_update=True)
194
194
  if agent_message.content_blocks and agent_message.content_blocks[0].contents:
195
195
  tool_blocks_map[tool_key] = agent_message.content_blocks[0].contents[-1]
196
196
  return agent_message, new_start_time
@@ -210,7 +210,7 @@ async def handle_on_tool_end(
210
210
 
211
211
  if tool_content and isinstance(tool_content, ToolContent):
212
212
  # Call send_message_method first to get the updated message structure
213
- agent_message = await send_message_method(message=agent_message)
213
+ agent_message = await send_message_method(message=agent_message, skip_db_update=True)
214
214
  new_start_time = perf_counter()
215
215
 
216
216
  # Now find and update the tool content in the current message
@@ -258,7 +258,7 @@ async def handle_on_tool_error(
258
258
  tool_content.error = event["data"].get("error", "Unknown error")
259
259
  tool_content.duration = _calculate_duration(start_time)
260
260
  tool_content.header = {"title": f"Error using **{tool_content.name}**", "icon": "Hammer"}
261
- agent_message = await send_message_method(message=agent_message)
261
+ agent_message = await send_message_method(message=agent_message, skip_db_update=True)
262
262
  start_time = perf_counter()
263
263
  return agent_message, start_time
264
264
 
@@ -275,14 +275,14 @@ async def handle_on_chain_stream(
275
275
  if output and isinstance(output, str | list):
276
276
  agent_message.text = _extract_output_text(output)
277
277
  agent_message.properties.state = "complete"
278
- agent_message = await send_message_method(message=agent_message)
278
+ agent_message = await send_message_method(message=agent_message, skip_db_update=True)
279
279
  start_time = perf_counter()
280
280
  elif isinstance(data_chunk, AIMessageChunk):
281
281
  output_text = _extract_output_text(data_chunk.content)
282
282
  if output_text and isinstance(agent_message.text, str):
283
283
  agent_message.text += output_text
284
284
  agent_message.properties.state = "partial"
285
- agent_message = await send_message_method(message=agent_message)
285
+ agent_message = await send_message_method(message=agent_message, skip_db_update=True)
286
286
  if not agent_message.text:
287
287
  start_time = perf_counter()
288
288
  return agent_message, start_time
@@ -346,13 +346,17 @@ async def process_agent_events(
346
346
  async for event in agent_executor:
347
347
  if event["event"] in TOOL_EVENT_HANDLERS:
348
348
  tool_handler = TOOL_EVENT_HANDLERS[event["event"]]
349
+ # Use skip_db_update=True during streaming to avoid DB round-trips
349
350
  agent_message, start_time = await tool_handler(
350
351
  event, agent_message, tool_blocks_map, send_message_method, start_time
351
352
  )
352
353
  elif event["event"] in CHAIN_EVENT_HANDLERS:
353
354
  chain_handler = CHAIN_EVENT_HANDLERS[event["event"]]
355
+ # Use skip_db_update=True during streaming to avoid DB round-trips
354
356
  agent_message, start_time = await chain_handler(event, agent_message, send_message_method, start_time)
355
357
  agent_message.properties.state = "complete"
358
+ # Final DB update with the complete message (skip_db_update=False by default)
359
+ agent_message = await send_message_method(message=agent_message)
356
360
  except Exception as e:
357
361
  raise ExceptionWithMessageError(agent_message, str(e)) from e
358
362
  return await Message.create(**agent_message.model_dump())
lfx/base/mcp/util.py CHANGED
@@ -28,8 +28,12 @@ HTTP_ERROR_STATUS_CODE = httpx_codes.BAD_REQUEST # HTTP status code for client
28
28
 
29
29
  # HTTP status codes used in validation
30
30
  HTTP_NOT_FOUND = 404
31
+ HTTP_METHOD_NOT_ALLOWED = 405
32
+ HTTP_NOT_ACCEPTABLE = 406
31
33
  HTTP_BAD_REQUEST = 400
32
34
  HTTP_INTERNAL_SERVER_ERROR = 500
35
+ HTTP_UNAUTHORIZED = 401
36
+ HTTP_FORBIDDEN = 403
33
37
 
34
38
  # MCP Session Manager constants
35
39
  settings = get_settings_service().settings
@@ -378,8 +382,8 @@ def _validate_node_installation(command: str) -> str:
378
382
 
379
383
  async def _validate_connection_params(mode: str, command: str | None = None, url: str | None = None) -> None:
380
384
  """Validate connection parameters based on mode."""
381
- if mode not in ["Stdio", "SSE"]:
382
- msg = f"Invalid mode: {mode}. Must be either 'Stdio' or 'SSE'"
385
+ if mode not in ["Stdio", "Streamable_HTTP", "SSE"]:
386
+ msg = f"Invalid mode: {mode}. Must be either 'Stdio', 'Streamable_HTTP', or 'SSE'"
383
387
  raise ValueError(msg)
384
388
 
385
389
  if mode == "Stdio" and not command:
@@ -387,8 +391,8 @@ async def _validate_connection_params(mode: str, command: str | None = None, url
387
391
  raise ValueError(msg)
388
392
  if mode == "Stdio" and command:
389
393
  _validate_node_installation(command)
390
- if mode == "SSE" and not url:
391
- msg = "URL is required for SSE mode"
394
+ if mode in ["Streamable_HTTP", "SSE"] and not url:
395
+ msg = f"URL is required for {mode} mode"
392
396
  raise ValueError(msg)
393
397
 
394
398
 
@@ -400,6 +404,7 @@ class MCPSessionManager:
400
404
  2. Maximum session limits per server to prevent resource exhaustion
401
405
  3. Idle timeout for automatic session cleanup
402
406
  4. Periodic cleanup of stale sessions
407
+ 5. Transport preference caching to avoid retrying failed transports
403
408
  """
404
409
 
405
410
  def __init__(self):
@@ -410,6 +415,9 @@ class MCPSessionManager:
410
415
  self._context_to_session: dict[str, tuple[str, str]] = {}
411
416
  # Reference count for each active (server_key, session_id)
412
417
  self._session_refcount: dict[tuple[str, str], int] = {}
418
+ # Cache which transport works for each server to avoid retrying failed transports
419
+ # server_key -> "streamable_http" | "sse"
420
+ self._transport_preference: dict[str, str] = {}
413
421
  self._cleanup_task = None
414
422
  self._start_cleanup_task()
415
423
 
@@ -467,15 +475,16 @@ class MCPSessionManager:
467
475
  env_str = str(sorted((connection_params.env or {}).items()))
468
476
  key_input = f"{command_str}|{env_str}"
469
477
  return f"stdio_{hash(key_input)}"
470
- elif transport_type == "sse" and (isinstance(connection_params, dict) and "url" in connection_params):
478
+ elif transport_type == "streamable_http" and (
479
+ isinstance(connection_params, dict) and "url" in connection_params
480
+ ):
471
481
  # Include URL and headers for uniqueness
472
482
  url = connection_params["url"]
473
483
  headers = str(sorted((connection_params.get("headers", {})).items()))
474
484
  key_input = f"{url}|{headers}"
475
- return f"sse_{hash(key_input)}"
485
+ return f"streamable_http_{hash(key_input)}"
476
486
 
477
487
  # Fallback to a generic key
478
- # TODO: add option for streamable HTTP in future.
479
488
  return f"{transport_type}_{hash(str(connection_params))}"
480
489
 
481
490
  async def _validate_session_connectivity(self, session) -> bool:
@@ -525,7 +534,7 @@ class MCPSessionManager:
525
534
  """Get or create a session with improved reuse strategy.
526
535
 
527
536
  The key insight is that we should reuse sessions based on the server
528
- identity (command + args for stdio, URL for SSE) rather than the context_id.
537
+ identity (command + args for stdio, URL for Streamable HTTP) rather than the context_id.
529
538
  This prevents creating a new subprocess for each unique context.
530
539
  """
531
540
  server_key = self._get_server_key(connection_params, transport_type)
@@ -578,17 +587,24 @@ class MCPSessionManager:
578
587
 
579
588
  if transport_type == "stdio":
580
589
  session, task = await self._create_stdio_session(session_id, connection_params)
581
- elif transport_type == "sse":
582
- session, task = await self._create_sse_session(session_id, connection_params)
590
+ actual_transport = "stdio"
591
+ elif transport_type == "streamable_http":
592
+ # Pass the cached transport preference if available
593
+ preferred_transport = self._transport_preference.get(server_key)
594
+ session, task, actual_transport = await self._create_streamable_http_session(
595
+ session_id, connection_params, preferred_transport
596
+ )
597
+ # Cache the transport that worked for future connections
598
+ self._transport_preference[server_key] = actual_transport
583
599
  else:
584
600
  msg = f"Unknown transport type: {transport_type}"
585
601
  raise ValueError(msg)
586
602
 
587
- # Store session info
603
+ # Store session info with the actual transport used
588
604
  sessions[session_id] = {
589
605
  "session": session,
590
606
  "task": task,
591
- "type": transport_type,
607
+ "type": actual_transport,
592
608
  "last_used": asyncio.get_event_loop().time(),
593
609
  }
594
610
 
@@ -634,9 +650,9 @@ class MCPSessionManager:
634
650
  self._background_tasks.add(task)
635
651
  task.add_done_callback(self._background_tasks.discard)
636
652
 
637
- # Wait for session to be ready
653
+ # Wait for session to be ready (use longer timeout for remote connections)
638
654
  try:
639
- session = await asyncio.wait_for(session_future, timeout=10.0)
655
+ session = await asyncio.wait_for(session_future, timeout=30.0)
640
656
  except asyncio.TimeoutError as timeout_err:
641
657
  # Clean up the failed task
642
658
  if not task.done():
@@ -652,50 +668,136 @@ class MCPSessionManager:
652
668
 
653
669
  return session, task
654
670
 
655
- async def _create_sse_session(self, session_id: str, connection_params):
656
- """Create a new SSE session as a background task to avoid context issues."""
671
+ async def _create_streamable_http_session(
672
+ self, session_id: str, connection_params, preferred_transport: str | None = None
673
+ ):
674
+ """Create a new Streamable HTTP session with SSE fallback as a background task to avoid context issues.
675
+
676
+ Args:
677
+ session_id: Unique identifier for this session
678
+ connection_params: Connection parameters including URL, headers, timeouts
679
+ preferred_transport: If set to "sse", skip Streamable HTTP and go directly to SSE
680
+
681
+ Returns:
682
+ tuple: (session, task, transport_used) where transport_used is "streamable_http" or "sse"
683
+ """
657
684
  import asyncio
658
685
 
659
686
  from mcp.client.sse import sse_client
687
+ from mcp.client.streamable_http import streamablehttp_client
660
688
 
661
689
  # Create a future to get the session
662
690
  session_future: asyncio.Future[ClientSession] = asyncio.Future()
691
+ # Track which transport succeeded
692
+ used_transport: list[str] = []
663
693
 
664
694
  async def session_task():
665
695
  """Background task that keeps the session alive."""
666
- try:
667
- async with sse_client(
668
- connection_params["url"],
669
- connection_params["headers"],
670
- connection_params["timeout_seconds"],
671
- connection_params["sse_read_timeout_seconds"],
672
- ) as (read, write):
673
- session = ClientSession(read, write)
674
- async with session:
675
- await session.initialize()
676
- # Signal that session is ready
677
- session_future.set_result(session)
696
+ streamable_error = None
678
697
 
679
- # Keep the session alive until cancelled
680
- import anyio
698
+ # Skip Streamable HTTP if we know SSE works for this server
699
+ if preferred_transport != "sse":
700
+ # Try Streamable HTTP first with a quick timeout
701
+ try:
702
+ await logger.adebug(f"Attempting Streamable HTTP connection for session {session_id}")
703
+ # Use a shorter timeout for the initial connection attempt (2 seconds)
704
+ async with streamablehttp_client(
705
+ url=connection_params["url"],
706
+ headers=connection_params["headers"],
707
+ timeout=connection_params["timeout_seconds"],
708
+ ) as (read, write, _):
709
+ session = ClientSession(read, write)
710
+ async with session:
711
+ # Initialize with a timeout to fail fast
712
+ await asyncio.wait_for(session.initialize(), timeout=2.0)
713
+ used_transport.append("streamable_http")
714
+ await logger.ainfo(f"Session {session_id} connected via Streamable HTTP")
715
+ # Signal that session is ready
716
+ session_future.set_result(session)
717
+
718
+ # Keep the session alive until cancelled
719
+ import anyio
720
+
721
+ event = anyio.Event()
722
+ try:
723
+ await event.wait()
724
+ except asyncio.CancelledError:
725
+ await logger.ainfo(f"Session {session_id} (Streamable HTTP) is shutting down")
726
+ except (asyncio.TimeoutError, Exception) as e: # noqa: BLE001
727
+ # If Streamable HTTP fails or times out, try SSE as fallback immediately
728
+ streamable_error = e
729
+ error_type = "timed out" if isinstance(e, asyncio.TimeoutError) else "failed"
730
+ await logger.awarning(
731
+ f"Streamable HTTP {error_type} for session {session_id}: {e}. Falling back to SSE..."
732
+ )
733
+ else:
734
+ await logger.adebug(f"Skipping Streamable HTTP for session {session_id}, using cached SSE preference")
681
735
 
682
- event = anyio.Event()
683
- try:
684
- await event.wait()
685
- except asyncio.CancelledError:
686
- await logger.ainfo(f"Session {session_id} is shutting down")
687
- except Exception as e: # noqa: BLE001
688
- if not session_future.done():
689
- session_future.set_exception(e)
736
+ # Try SSE if Streamable HTTP failed or if SSE is preferred
737
+ if streamable_error is not None or preferred_transport == "sse":
738
+ try:
739
+ await logger.adebug(f"Attempting SSE connection for session {session_id}")
740
+ # Extract SSE read timeout from connection params, default to 30s if not present
741
+ sse_read_timeout = connection_params.get("sse_read_timeout_seconds", 30)
742
+
743
+ async with sse_client(
744
+ connection_params["url"],
745
+ connection_params["headers"],
746
+ connection_params["timeout_seconds"],
747
+ sse_read_timeout,
748
+ ) as (read, write):
749
+ session = ClientSession(read, write)
750
+ async with session:
751
+ await session.initialize()
752
+ used_transport.append("sse")
753
+ fallback_msg = " (fallback)" if streamable_error else " (preferred)"
754
+ await logger.ainfo(f"Session {session_id} connected via SSE{fallback_msg}")
755
+ # Signal that session is ready
756
+ if not session_future.done():
757
+ session_future.set_result(session)
758
+
759
+ # Keep the session alive until cancelled
760
+ import anyio
761
+
762
+ event = anyio.Event()
763
+ try:
764
+ await event.wait()
765
+ except asyncio.CancelledError:
766
+ await logger.ainfo(f"Session {session_id} (SSE) is shutting down")
767
+ except Exception as sse_error: # noqa: BLE001
768
+ # Both transports failed (or just SSE if it was preferred)
769
+ if streamable_error:
770
+ await logger.aerror(
771
+ f"Both Streamable HTTP and SSE failed for session {session_id}. "
772
+ f"Streamable HTTP error: {streamable_error}. SSE error: {sse_error}"
773
+ )
774
+ if not session_future.done():
775
+ session_future.set_exception(
776
+ ValueError(
777
+ f"Failed to connect via Streamable HTTP ({streamable_error}) or SSE ({sse_error})"
778
+ )
779
+ )
780
+ else:
781
+ await logger.aerror(f"SSE connection failed for session {session_id}: {sse_error}")
782
+ if not session_future.done():
783
+ session_future.set_exception(ValueError(f"Failed to connect via SSE: {sse_error}"))
690
784
 
691
785
  # Start the background task
692
786
  task = asyncio.create_task(session_task())
693
787
  self._background_tasks.add(task)
694
788
  task.add_done_callback(self._background_tasks.discard)
695
789
 
696
- # Wait for session to be ready
790
+ # Wait for session to be ready (use longer timeout for remote connections)
697
791
  try:
698
- session = await asyncio.wait_for(session_future, timeout=10.0)
792
+ session = await asyncio.wait_for(session_future, timeout=30.0)
793
+ # Log which transport was used
794
+ if used_transport:
795
+ transport_used = used_transport[0]
796
+ await logger.ainfo(f"Session {session_id} successfully established using {transport_used}")
797
+ return session, task, transport_used
798
+ # This shouldn't happen, but handle it just in case
799
+ msg = f"Session {session_id} established but transport not recorded"
800
+ raise ValueError(msg)
699
801
  except asyncio.TimeoutError as timeout_err:
700
802
  # Clean up the failed task
701
803
  if not task.done():
@@ -705,12 +807,10 @@ class MCPSessionManager:
705
807
  with contextlib.suppress(asyncio.CancelledError):
706
808
  await task
707
809
  self._background_tasks.discard(task)
708
- msg = f"Timeout waiting for SSE session {session_id} to initialize"
810
+ msg = f"Timeout waiting for Streamable HTTP/SSE session {session_id} to initialize"
709
811
  await logger.aerror(msg)
710
812
  raise ValueError(msg) from timeout_err
711
813
 
712
- return session, task
713
-
714
814
  async def _cleanup_session_by_id(self, server_key: str, session_id: str):
715
815
  """Clean up a specific session by server key and session ID."""
716
816
  if server_key not in self.sessions_by_server:
@@ -1056,7 +1156,7 @@ class MCPStdioClient:
1056
1156
  await self.disconnect()
1057
1157
 
1058
1158
 
1059
- class MCPSseClient:
1159
+ class MCPStreamableHttpClient:
1060
1160
  def __init__(self, component_cache=None):
1061
1161
  self.session: ClientSession | None = None
1062
1162
  self._connection_params = None
@@ -1080,67 +1180,15 @@ class MCPSseClient:
1080
1180
  self._component_cache.set("mcp_session_manager", session_manager)
1081
1181
  return session_manager
1082
1182
 
1083
- async def validate_url(self, url: str | None, headers: dict[str, str] | None = None) -> tuple[bool, str]:
1084
- """Validate the SSE URL before attempting connection."""
1183
+ async def validate_url(self, url: str | None) -> tuple[bool, str]:
1184
+ """Validate the Streamable HTTP URL before attempting connection."""
1085
1185
  try:
1086
1186
  parsed = urlparse(url)
1087
1187
  if not parsed.scheme or not parsed.netloc:
1088
1188
  return False, "Invalid URL format. Must include scheme (http/https) and host."
1089
-
1090
- async with httpx.AsyncClient() as client:
1091
- try:
1092
- # For SSE endpoints, try a GET request with short timeout
1093
- # Many SSE servers don't support HEAD requests and return 404
1094
- response = await client.get(
1095
- url, timeout=2.0, headers={"Accept": "text/event-stream", **(headers or {})}
1096
- )
1097
-
1098
- # For SSE, we expect the server to either:
1099
- # 1. Start streaming (200)
1100
- # 2. Return 404 if HEAD/GET without proper SSE handshake is not supported
1101
- # 3. Return other status codes that we should handle gracefully
1102
-
1103
- # Don't fail on 404 since many SSE endpoints return this for non-SSE requests
1104
- if response.status_code == HTTP_NOT_FOUND:
1105
- # This is likely an SSE endpoint that doesn't support regular GET
1106
- # Let the actual SSE connection attempt handle this
1107
- return True, ""
1108
-
1109
- # Fail on client errors except 404, but allow server errors and redirects
1110
- if (
1111
- HTTP_BAD_REQUEST <= response.status_code < HTTP_INTERNAL_SERVER_ERROR
1112
- and response.status_code != HTTP_NOT_FOUND
1113
- ):
1114
- return False, f"Server returned client error status: {response.status_code}"
1115
-
1116
- except httpx.TimeoutException:
1117
- # Timeout on a short request might indicate the server is trying to stream
1118
- # This is actually expected behavior for SSE endpoints
1119
- return True, ""
1120
- except httpx.NetworkError:
1121
- return False, "Network error. Could not reach the server."
1122
- else:
1123
- return True, ""
1124
-
1125
- except (httpx.HTTPError, ValueError, OSError) as e:
1189
+ except (ValueError, OSError) as e:
1126
1190
  return False, f"URL validation error: {e!s}"
1127
-
1128
- async def pre_check_redirect(self, url: str | None, headers: dict[str, str] | None = None) -> str | None:
1129
- """Check for redirects and return the final URL."""
1130
- if url is None:
1131
- return url
1132
- try:
1133
- async with httpx.AsyncClient(follow_redirects=False) as client:
1134
- # Use GET with SSE headers instead of HEAD since many SSE servers don't support HEAD
1135
- response = await client.get(
1136
- url, timeout=2.0, headers={"Accept": "text/event-stream", **(headers or {})}
1137
- )
1138
- if response.status_code == httpx.codes.TEMPORARY_REDIRECT:
1139
- return response.headers.get("Location", url)
1140
- # Don't treat 404 as an error here - let the main connection handle it
1141
- except (httpx.RequestError, httpx.HTTPError) as e:
1142
- await logger.awarning(f"Error checking redirects: {e}")
1143
- return url
1191
+ return True, ""
1144
1192
 
1145
1193
  async def _connect_to_server(
1146
1194
  self,
@@ -1149,27 +1197,31 @@ class MCPSseClient:
1149
1197
  timeout_seconds: int = 30,
1150
1198
  sse_read_timeout_seconds: int = 30,
1151
1199
  ) -> list[StructuredTool]:
1152
- """Connect to MCP server using SSE transport (SDK style)."""
1200
+ """Connect to MCP server using Streamable HTTP transport with SSE fallback (SDK style)."""
1153
1201
  # Validate and sanitize headers early
1154
1202
  validated_headers = _process_headers(headers)
1155
1203
 
1156
1204
  if url is None:
1157
- msg = "URL is required for SSE mode"
1205
+ msg = "URL is required for StreamableHTTP or SSE mode"
1158
1206
  raise ValueError(msg)
1159
- is_valid, error_msg = await self.validate_url(url, validated_headers)
1160
- if not is_valid:
1161
- msg = f"Invalid SSE URL ({url}): {error_msg}"
1162
- raise ValueError(msg)
1163
-
1164
- url = await self.pre_check_redirect(url, validated_headers)
1165
1207
 
1166
- # Store connection parameters for later use in run_tool
1167
- self._connection_params = {
1168
- "url": url,
1169
- "headers": validated_headers,
1170
- "timeout_seconds": timeout_seconds,
1171
- "sse_read_timeout_seconds": sse_read_timeout_seconds,
1172
- }
1208
+ # Only validate URL if we don't have a cached session
1209
+ # This avoids expensive HTTP validation calls when reusing sessions
1210
+ if not self._connected or not self._connection_params:
1211
+ is_valid, error_msg = await self.validate_url(url)
1212
+ if not is_valid:
1213
+ msg = f"Invalid Streamable HTTP or SSE URL ({url}): {error_msg}"
1214
+ raise ValueError(msg)
1215
+ # Store connection parameters for later use in run_tool
1216
+ # Include SSE read timeout for fallback
1217
+ self._connection_params = {
1218
+ "url": url,
1219
+ "headers": validated_headers,
1220
+ "timeout_seconds": timeout_seconds,
1221
+ "sse_read_timeout_seconds": sse_read_timeout_seconds,
1222
+ }
1223
+ elif headers:
1224
+ self._connection_params["headers"] = validated_headers
1173
1225
 
1174
1226
  # If no session context is set, create a default one
1175
1227
  if not self._session_context:
@@ -1177,18 +1229,21 @@ class MCPSseClient:
1177
1229
  import uuid
1178
1230
 
1179
1231
  param_hash = uuid.uuid4().hex[:8]
1180
- self._session_context = f"default_sse_{param_hash}"
1232
+ self._session_context = f"default_http_{param_hash}"
1181
1233
 
1182
- # Get or create a persistent session
1234
+ # Get or create a persistent session (will try Streamable HTTP, then SSE fallback)
1183
1235
  session = await self._get_or_create_session()
1184
1236
  response = await session.list_tools()
1185
1237
  self._connected = True
1186
1238
  return response.tools
1187
1239
 
1188
- async def connect_to_server(self, url: str, headers: dict[str, str] | None = None) -> list[StructuredTool]:
1189
- """Connect to MCP server using SSE transport (SDK style)."""
1240
+ async def connect_to_server(
1241
+ self, url: str, headers: dict[str, str] | None = None, sse_read_timeout_seconds: int = 30
1242
+ ) -> list[StructuredTool]:
1243
+ """Connect to MCP server using Streamable HTTP with SSE fallback transport (SDK style)."""
1190
1244
  return await asyncio.wait_for(
1191
- self._connect_to_server(url, headers), timeout=get_settings_service().settings.mcp_server_timeout
1245
+ self._connect_to_server(url, headers, sse_read_timeout_seconds=sse_read_timeout_seconds),
1246
+ timeout=get_settings_service().settings.mcp_server_timeout,
1192
1247
  )
1193
1248
 
1194
1249
  def set_session_context(self, context_id: str):
@@ -1204,12 +1259,14 @@ class MCPSseClient:
1204
1259
  # Use cached session manager to get/create persistent session
1205
1260
  session_manager = self._get_session_manager()
1206
1261
  # Cache session so we can access server-assigned session_id later for DELETE
1207
- self.session = await session_manager.get_session(self._session_context, self._connection_params, "sse")
1262
+ self.session = await session_manager.get_session(
1263
+ self._session_context, self._connection_params, "streamable_http"
1264
+ )
1208
1265
  return self.session
1209
1266
 
1210
1267
  async def _terminate_remote_session(self) -> None:
1211
1268
  """Attempt to explicitly terminate the remote MCP session via HTTP DELETE (best-effort)."""
1212
- # Only relevant for SSE transport
1269
+ # Only relevant for Streamable HTTP or SSE transport
1213
1270
  if not self._connection_params or "url" not in self._connection_params:
1214
1271
  return
1215
1272
 
@@ -1255,7 +1312,7 @@ class MCPSseClient:
1255
1312
  import uuid
1256
1313
 
1257
1314
  param_hash = uuid.uuid4().hex[:8]
1258
- self._session_context = f"default_sse_{param_hash}"
1315
+ self._session_context = f"default_http_{param_hash}"
1259
1316
 
1260
1317
  max_retries = 2
1261
1318
  last_error_type = None
@@ -1326,7 +1383,7 @@ class MCPSseClient:
1326
1383
  await logger.aerror(msg)
1327
1384
  # Clean up failed session from cache
1328
1385
  if self._session_context and self._component_cache:
1329
- cache_key = f"mcp_session_sse_{self._session_context}"
1386
+ cache_key = f"mcp_session_http_{self._session_context}"
1330
1387
  self._component_cache.delete(cache_key)
1331
1388
  self._connected = False
1332
1389
  raise ValueError(msg) from e
@@ -1364,11 +1421,17 @@ class MCPSseClient:
1364
1421
  await self.disconnect()
1365
1422
 
1366
1423
 
1424
+ # Backward compatibility: MCPSseClient is now an alias for MCPStreamableHttpClient
1425
+ # The new client supports both Streamable HTTP and SSE with automatic fallback
1426
+ MCPSseClient = MCPStreamableHttpClient
1427
+
1428
+
1367
1429
  async def update_tools(
1368
1430
  server_name: str,
1369
1431
  server_config: dict,
1370
1432
  mcp_stdio_client: MCPStdioClient | None = None,
1371
- mcp_sse_client: MCPSseClient | None = None,
1433
+ mcp_streamable_http_client: MCPStreamableHttpClient | None = None,
1434
+ mcp_sse_client: MCPStreamableHttpClient | None = None, # Backward compatibility
1372
1435
  ) -> tuple[str, list[StructuredTool], dict[str, StructuredTool]]:
1373
1436
  """Fetch server config and update available tools."""
1374
1437
  if server_config is None:
@@ -1377,11 +1440,17 @@ async def update_tools(
1377
1440
  return "", [], {}
1378
1441
  if mcp_stdio_client is None:
1379
1442
  mcp_stdio_client = MCPStdioClient()
1380
- if mcp_sse_client is None:
1381
- mcp_sse_client = MCPSseClient()
1443
+
1444
+ # Backward compatibility: accept mcp_sse_client parameter
1445
+ if mcp_streamable_http_client is None:
1446
+ mcp_streamable_http_client = mcp_sse_client if mcp_sse_client is not None else MCPStreamableHttpClient()
1382
1447
 
1383
1448
  # Fetch server config from backend
1384
- mode = "Stdio" if "command" in server_config else "SSE" if "url" in server_config else ""
1449
+ # Determine mode from config, defaulting to Streamable_HTTP if URL present
1450
+ mode = server_config.get("mode", "")
1451
+ if not mode:
1452
+ mode = "Stdio" if "command" in server_config else "Streamable_HTTP" if "url" in server_config else ""
1453
+
1385
1454
  command = server_config.get("command", "")
1386
1455
  url = server_config.get("url", "")
1387
1456
  tools = []
@@ -1394,7 +1463,7 @@ async def update_tools(
1394
1463
  raise
1395
1464
 
1396
1465
  # Determine connection type and parameters
1397
- client: MCPStdioClient | MCPSseClient | None = None
1466
+ client: MCPStdioClient | MCPStreamableHttpClient | None = None
1398
1467
  if mode == "Stdio":
1399
1468
  # Stdio connection
1400
1469
  args = server_config.get("args", [])
@@ -1402,10 +1471,10 @@ async def update_tools(
1402
1471
  full_command = " ".join([command, *args])
1403
1472
  tools = await mcp_stdio_client.connect_to_server(full_command, env)
1404
1473
  client = mcp_stdio_client
1405
- elif mode == "SSE":
1406
- # SSE connection
1407
- tools = await mcp_sse_client.connect_to_server(url, headers=headers)
1408
- client = mcp_sse_client
1474
+ elif mode in ["Streamable_HTTP", "SSE"]:
1475
+ # Streamable HTTP connection with SSE fallback
1476
+ tools = await mcp_streamable_http_client.connect_to_server(url, headers=headers)
1477
+ client = mcp_streamable_http_client
1409
1478
  else:
1410
1479
  logger.error(f"Invalid MCP server mode for '{server_name}': {mode}")
1411
1480
  return "", [], {}
lfx/base/models/model.py CHANGED
@@ -229,7 +229,7 @@ class LCModelComponent(Component):
229
229
  system_message_added = True
230
230
  runnable = prompt | runnable
231
231
  else:
232
- messages.append(input_value.to_lc_message())
232
+ messages.append(input_value.to_lc_message(self.name))
233
233
  else:
234
234
  messages.append(HumanMessage(content=input_value))
235
235
 
@@ -7,7 +7,12 @@ from typing import Any
7
7
  from langchain_core.tools import StructuredTool # noqa: TC002
8
8
 
9
9
  from lfx.base.agents.utils import maybe_unflatten_dict, safe_cache_get, safe_cache_set
10
- from lfx.base.mcp.util import MCPSseClient, MCPStdioClient, create_input_schema_from_json_schema, update_tools
10
+ from lfx.base.mcp.util import (
11
+ MCPStdioClient,
12
+ MCPStreamableHttpClient,
13
+ create_input_schema_from_json_schema,
14
+ update_tools,
15
+ )
11
16
  from lfx.custom.custom_component.component_with_cache import ComponentWithCache
12
17
  from lfx.inputs.inputs import InputTypes # noqa: TC001
13
18
  from lfx.io import BoolInput, DropdownInput, McpInput, MessageTextInput, Output
@@ -32,7 +37,9 @@ class MCPToolsComponent(ComponentWithCache):
32
37
 
33
38
  # Initialize clients with access to the component cache
34
39
  self.stdio_client: MCPStdioClient = MCPStdioClient(component_cache=self._shared_component_cache)
35
- self.sse_client: MCPSseClient = MCPSseClient(component_cache=self._shared_component_cache)
40
+ self.streamable_http_client: MCPStreamableHttpClient = MCPStreamableHttpClient(
41
+ component_cache=self._shared_component_cache
42
+ )
36
43
 
37
44
  def _ensure_cache_structure(self):
38
45
  """Ensure the cache has the required structure."""
@@ -207,7 +214,7 @@ class MCPToolsComponent(ComponentWithCache):
207
214
  server_name=server_name,
208
215
  server_config=server_config,
209
216
  mcp_stdio_client=self.stdio_client,
210
- mcp_sse_client=self.sse_client,
217
+ mcp_streamable_http_client=self.streamable_http_client,
211
218
  )
212
219
 
213
220
  self.tool_names = [tool.name for tool in tool_list if hasattr(tool, "name")]
@@ -496,7 +503,7 @@ class MCPToolsComponent(ComponentWithCache):
496
503
  session_context = self._get_session_context()
497
504
  if session_context:
498
505
  self.stdio_client.set_session_context(session_context)
499
- self.sse_client.set_session_context(session_context)
506
+ self.streamable_http_client.set_session_context(session_context)
500
507
 
501
508
  exec_tool = self._tool_cache[self.tool]
502
509
  tool_args = self.get_inputs_for_all_tools(self.tools)[self.tool]
@@ -24,11 +24,8 @@ class NVIDIAModelComponent(LCModelComponent):
24
24
  except ImportError as e:
25
25
  msg = "Please install langchain-nvidia-ai-endpoints to use the NVIDIA model."
26
26
  raise ImportError(msg) from e
27
- except Exception: # noqa: BLE001
28
- logger.warning(
29
- "Failed to connect to NVIDIA API. Model list may be unavailable."
30
- " Please check your internet connection and API credentials."
31
- )
27
+ except Exception as e: # noqa: BLE001
28
+ logger.warning(f"Failed to fetch NVIDIA models during initialization: {e}. Model list will be unavailable.")
32
29
  all_models = []
33
30
 
34
31
  inputs = [
@@ -32,8 +32,8 @@ class ChatOllamaComponent(LCModelComponent):
32
32
  MessageTextInput(
33
33
  name="base_url",
34
34
  display_name="Base URL",
35
- info="Endpoint of the Ollama API.",
36
- value="",
35
+ info="Endpoint of the Ollama API. Defaults to http://localhost:11434 .",
36
+ value="http://localhost:11434",
37
37
  real_time_refresh=True,
38
38
  ),
39
39
  DropdownInput(
@@ -157,6 +157,18 @@ class ChatOllamaComponent(LCModelComponent):
157
157
  mirostat_tau = self.mirostat_tau
158
158
 
159
159
  transformed_base_url = transform_localhost_url(self.base_url)
160
+
161
+ # Check if URL contains /v1 suffix (OpenAI-compatible mode)
162
+ if transformed_base_url and transformed_base_url.rstrip("/").endswith("/v1"):
163
+ # Strip /v1 suffix and log warning
164
+ transformed_base_url = transformed_base_url.rstrip("/").removesuffix("/v1")
165
+ logger.warning(
166
+ "Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, "
167
+ "not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. "
168
+ "If you want to use the OpenAI-compatible API, please use the OpenAI component instead. "
169
+ "Learn more at https://docs.ollama.com/openai#openai-compatibility"
170
+ )
171
+
160
172
  # Mapping system settings to their corresponding values
161
173
  llm_params = {
162
174
  "base_url": transformed_base_url,
@@ -190,8 +202,8 @@ class ChatOllamaComponent(LCModelComponent):
190
202
  output = ChatOllama(**llm_params)
191
203
  except Exception as e:
192
204
  msg = (
193
- "Unable to connect to the Ollama API. ",
194
- "Please verify the base URL, ensure the relevant Ollama model is pulled, and try again.",
205
+ "Unable to connect to the Ollama API. "
206
+ "Please verify the base URL, ensure the relevant Ollama model is pulled, and try again."
195
207
  )
196
208
  raise ValueError(msg) from e
197
209
 
@@ -201,6 +213,12 @@ class ChatOllamaComponent(LCModelComponent):
201
213
  try:
202
214
  async with httpx.AsyncClient() as client:
203
215
  url = transform_localhost_url(url)
216
+ if not url:
217
+ return False
218
+ # Strip /v1 suffix if present, as Ollama API endpoints are at root level
219
+ url = url.rstrip("/").removesuffix("/v1")
220
+ if not url.endswith("/"):
221
+ url = url + "/"
204
222
  return (await client.get(urljoin(url, "api/tags"))).status_code == HTTP_STATUS_OK
205
223
  except httpx.RequestError:
206
224
  return False
@@ -224,9 +242,6 @@ class ChatOllamaComponent(LCModelComponent):
224
242
  build_config["mirostat_eta"]["value"] = 0.1
225
243
  build_config["mirostat_tau"]["value"] = 5
226
244
 
227
- if field_name in {"base_url", "model_name"} and not await self.is_valid_ollama_url(self.base_url):
228
- msg = "Ollama is not running on the provided base URL. Please start Ollama and try again."
229
- raise ValueError(msg)
230
245
  if field_name in {"model_name", "base_url", "tool_model_enabled"}:
231
246
  if await self.is_valid_ollama_url(self.base_url):
232
247
  tool_model_enabled = build_config["tool_model_enabled"].get("value", False) or self.tool_model_enabled
@@ -264,8 +279,10 @@ class ChatOllamaComponent(LCModelComponent):
264
279
  names cannot be retrieved.
265
280
  """
266
281
  try:
267
- # Normalize the base URL to avoid the repeated "/" at the end
268
- base_url = base_url_value.rstrip("/") + "/"
282
+ # Strip /v1 suffix if present, as Ollama API endpoints are at root level
283
+ base_url = base_url_value.rstrip("/").removesuffix("/v1")
284
+ if not base_url.endswith("/"):
285
+ base_url = base_url + "/"
269
286
  base_url = transform_localhost_url(base_url)
270
287
 
271
288
  # Ollama REST API to return models
@@ -1548,7 +1548,16 @@ class Component(CustomComponent):
1548
1548
  if not message.sender_name:
1549
1549
  message.sender_name = MESSAGE_SENDER_NAME_AI
1550
1550
 
1551
- async def send_message(self, message: Message, id_: str | None = None):
1551
+ async def send_message(self, message: Message, id_: str | None = None, *, skip_db_update: bool = False):
1552
+ """Send a message with optional database update control.
1553
+
1554
+ Args:
1555
+ message: The message to send
1556
+ id_: Optional message ID
1557
+ skip_db_update: If True, only update in-memory and send event, skip DB write.
1558
+ Useful during streaming to avoid excessive DB round-trips.
1559
+ Note: This assumes the message already exists in the database with message.id set.
1560
+ """
1552
1561
  if self._should_skip_message(message):
1553
1562
  return message
1554
1563
 
@@ -1558,26 +1567,37 @@ class Component(CustomComponent):
1558
1567
  # Ensure required fields for message storage are set
1559
1568
  self._ensure_message_required_fields(message)
1560
1569
 
1561
- stored_message = await self._store_message(message)
1570
+ # If skip_db_update is True and message already has an ID, skip the DB write
1571
+ # This path is used during agent streaming to avoid excessive DB round-trips
1572
+ if skip_db_update and message.id:
1573
+ # Create a fresh Message instance for consistency with normal flow
1574
+ stored_message = await Message.create(**message.model_dump())
1575
+ self._stored_message_id = stored_message.id
1576
+ # Still send the event to update the client in real-time
1577
+ # Note: If this fails, we don't need DB cleanup since we didn't write to DB
1578
+ await self._send_message_event(stored_message, id_=id_)
1579
+ else:
1580
+ # Normal flow: store/update in database
1581
+ stored_message = await self._store_message(message)
1562
1582
 
1563
- self._stored_message_id = stored_message.id
1564
- try:
1565
- complete_message = ""
1566
- if (
1567
- self._should_stream_message(stored_message, message)
1568
- and message is not None
1569
- and isinstance(message.text, AsyncIterator | Iterator)
1570
- ):
1571
- complete_message = await self._stream_message(message.text, stored_message)
1572
- stored_message.text = complete_message
1573
- stored_message = await self._update_stored_message(stored_message)
1574
- else:
1575
- # Only send message event for non-streaming messages
1576
- await self._send_message_event(stored_message, id_=id_)
1577
- except Exception:
1578
- # remove the message from the database
1579
- await delete_message(stored_message.id)
1580
- raise
1583
+ self._stored_message_id = stored_message.id
1584
+ try:
1585
+ complete_message = ""
1586
+ if (
1587
+ self._should_stream_message(stored_message, message)
1588
+ and message is not None
1589
+ and isinstance(message.text, AsyncIterator | Iterator)
1590
+ ):
1591
+ complete_message = await self._stream_message(message.text, stored_message)
1592
+ stored_message.text = complete_message
1593
+ stored_message = await self._update_stored_message(stored_message)
1594
+ else:
1595
+ # Only send message event for non-streaming messages
1596
+ await self._send_message_event(stored_message, id_=id_)
1597
+ except Exception:
1598
+ # remove the message from the database
1599
+ await delete_message(stored_message.id)
1600
+ raise
1581
1601
  self.status = stored_message
1582
1602
  return stored_message
1583
1603
 
@@ -95,8 +95,10 @@ def _process_single_module(modname: str) -> tuple[str, dict] | None:
95
95
  """
96
96
  try:
97
97
  module = importlib.import_module(modname)
98
- except (ImportError, AttributeError) as e:
99
- logger.error(f"Error importing module {modname}: {e}", exc_info=True)
98
+ except Exception as e: # noqa: BLE001
99
+ # Catch all exceptions during import to prevent component failures from crashing startup
100
+ # TODO: Surface these errors to the UI in a friendly manner
101
+ logger.error(f"Failed to import module {modname}: {e}", exc_info=True)
100
102
  return None
101
103
  # Extract the top-level subpackage name after "langflow.components."
102
104
  # e.g., "langflow.components.Notion.add_content_to_page" -> "Notion"
lfx/schema/log.py CHANGED
@@ -34,6 +34,7 @@ class SendMessageFunctionType(Protocol):
34
34
  id_: str | None = None,
35
35
  *,
36
36
  allow_markdown: bool = True,
37
+ skip_db_update: bool = False,
37
38
  ) -> Message: ...
38
39
 
39
40
 
lfx/schema/message.py CHANGED
@@ -121,9 +121,13 @@ class Message(Data):
121
121
 
122
122
  def to_lc_message(
123
123
  self,
124
+ model_name: str | None = None,
124
125
  ) -> BaseMessage:
125
126
  """Converts the Data to a BaseMessage.
126
127
 
128
+ Args:
129
+ model_name: The model name to use for conversion. Optional.
130
+
127
131
  Returns:
128
132
  BaseMessage: The converted BaseMessage.
129
133
  """
@@ -139,7 +143,7 @@ class Message(Data):
139
143
  if self.sender == MESSAGE_SENDER_USER or not self.sender:
140
144
  if self.files:
141
145
  contents = [{"type": "text", "text": text}]
142
- file_contents = self.get_file_content_dicts()
146
+ file_contents = self.get_file_content_dicts(model_name)
143
147
  contents.extend(file_contents)
144
148
  human_message = HumanMessage(content=contents)
145
149
  else:
@@ -197,7 +201,7 @@ class Message(Data):
197
201
  return value
198
202
 
199
203
  # Keep this async method for backwards compatibility
200
- def get_file_content_dicts(self):
204
+ def get_file_content_dicts(self, model_name: str | None = None):
201
205
  content_dicts = []
202
206
  try:
203
207
  files = get_file_paths(self.files)
@@ -209,7 +213,7 @@ class Message(Data):
209
213
  if isinstance(file, Image):
210
214
  content_dicts.append(file.to_content_dict())
211
215
  else:
212
- content_dicts.append(create_image_content_dict(file))
216
+ content_dicts.append(create_image_content_dict(file, None, model_name))
213
217
  return content_dicts
214
218
 
215
219
  def load_lc_prompt(self):
lfx/utils/image.py CHANGED
@@ -56,12 +56,15 @@ def create_data_url(image_path: str | Path, mime_type: str | None = None) -> str
56
56
 
57
57
 
58
58
  @lru_cache(maxsize=50)
59
- def create_image_content_dict(image_path: str | Path, mime_type: str | None = None) -> dict:
59
+ def create_image_content_dict(
60
+ image_path: str | Path, mime_type: str | None = None, model_name: str | None = None
61
+ ) -> dict:
60
62
  """Create a content dictionary for multimodal inputs from an image file.
61
63
 
62
64
  Args:
63
65
  image_path: Path to the image file
64
66
  mime_type: MIME type of the image. If None, will be auto-detected
67
+ model_name: Optional model parameter to determine content dict structure
65
68
 
66
69
  Returns:
67
70
  Content dictionary with type and image_url fields
@@ -70,4 +73,7 @@ def create_image_content_dict(image_path: str | Path, mime_type: str | None = No
70
73
  FileNotFoundError: If the image file doesn't exist
71
74
  """
72
75
  data_url = create_data_url(image_path, mime_type)
76
+
77
+ if model_name == "OllamaModel":
78
+ return {"type": "image_url", "source_type": "url", "image_url": data_url}
73
79
  return {"type": "image", "source_type": "url", "url": data_url}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lfx-nightly
3
- Version: 0.1.12.dev32
3
+ Version: 0.1.12.dev34
4
4
  Summary: Langflow Executor - A lightweight CLI tool for executing and serving Langflow AI flows
5
5
  Author-email: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
6
6
  Requires-Python: <3.14,>=3.10
@@ -12,7 +12,7 @@ lfx/base/agents/callback.py,sha256=mjlT9ukBMVrfjYrHsJowqpY4g9hVGBVBIYhncLWr3tQ,3
12
12
  lfx/base/agents/context.py,sha256=u0wboX1aRR22Ia8gY14WF12RjhE0Rxv9hPBiixT9DtQ,3916
13
13
  lfx/base/agents/default_prompts.py,sha256=tUjfczwt4D5R1KozNOl1uSL2V2rSCZeUMS-cfV4Gwn0,955
14
14
  lfx/base/agents/errors.py,sha256=4QY1AqSWZaOjq-iQRYH_aeCfH_hWECLQkiwybNXz66U,531
15
- lfx/base/agents/events.py,sha256=oMIrTi6-5AhL_NBY3XSH24Yt1adjDBXgSPeIaedSEIQ,14126
15
+ lfx/base/agents/events.py,sha256=jOhfGf_za5IYMvxRMvi1qqU0gw9b1imvDCOQP3r3wlo,14598
16
16
  lfx/base/agents/utils.py,sha256=OcmtZx4BTFTyq2A3rta3WoJn98UzEYdEXoRLs8-mTVo,6511
17
17
  lfx/base/agents/crewai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  lfx/base/agents/crewai/crew.py,sha256=TN1JyLXMpJc2yPH3tokhFmxKKYoJ4lMvmG19DmpKfeY,7953
@@ -52,7 +52,7 @@ lfx/base/langwatch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
52
52
  lfx/base/langwatch/utils.py,sha256=N7rH3sRwgmNQzG0pKjj4wr_ans_drwtvkx4BQt-B0WA,457
53
53
  lfx/base/mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  lfx/base/mcp/constants.py,sha256=-1XnJxejlqm9zs1R91qGtOeX-_F1ZpdHVzCIqUCvhgE,62
55
- lfx/base/mcp/util.py,sha256=RbsZ8HPuN04aTNaGUgojHCUaMVWos_rhOP554wv4Fg0,64603
55
+ lfx/base/mcp/util.py,sha256=rHi-wC8tuUWCiI65AylJNE7dmO59K71R_W-ruN3MO50,69164
56
56
  lfx/base/memory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  lfx/base/memory/memory.py,sha256=kZ-aZoHvRW4PJAgY1LUt5UBj7YXbos_aVBPGjC1EFCY,1835
58
58
  lfx/base/memory/model.py,sha256=2oDORZV_l-DHLx9j9--wYprQUIYKOb8aTJpXmR1qOLw,1330
@@ -63,7 +63,7 @@ lfx/base/models/aws_constants.py,sha256=-Fa7T3wJqBaZhs80ATRgZP6yZ0Nsd1YYdZv9SfqT
63
63
  lfx/base/models/chat_result.py,sha256=-MypS6_GKXOqWevtk0xwtrsEO4mIgpPAt7-EML5n0vA,2756
64
64
  lfx/base/models/google_generative_ai_constants.py,sha256=EuFd77ZrrSr6YtSKtmEaq0Nfa4y45AbDe_cz_18nReE,2564
65
65
  lfx/base/models/groq_constants.py,sha256=WOMpYRwJVrZavsi7zGJwRHJX8ZBvdtILUOmBFv0QIPQ,5536
66
- lfx/base/models/model.py,sha256=Z-qAzfzQoGILvskF60fIvjfiwPq27OtHSVHDTPNwbHE,15315
66
+ lfx/base/models/model.py,sha256=nJgExAvMJ5WMxCqC__Jc1GdkgJag4yrwC9nFtPEVupM,15324
67
67
  lfx/base/models/model_input_constants.py,sha256=WrnkAmMTk4cMjjLgBzRffJDzow7LWRpfc5GsgdRxvU4,10748
68
68
  lfx/base/models/model_metadata.py,sha256=tNFPiRqBJ0WPKdNEqBxuoKk0n8H_h0J--bCV5pk9k4o,1325
69
69
  lfx/base/models/model_utils.py,sha256=RwXUSIw5gdRakQ-VGbLI1iT0CeeWrVSNTgUQIrrc6uE,474
@@ -111,7 +111,7 @@ lfx/components/agentql/__init__.py,sha256=Erl669Dzsk-SegsDPWTtkKbprMXVuv8UTCo5RE
111
111
  lfx/components/agentql/agentql_api.py,sha256=N94yEK7ZuQCIsFBlr_8dqrJY-K1-KNb6QEEYfDIsDME,5569
112
112
  lfx/components/agents/__init__.py,sha256=u1PH9Ui0dUgTdTZVP7cdVysCv4extdusKS_brcbE7Eg,1049
113
113
  lfx/components/agents/agent.py,sha256=gUZ5RTgf_6G8Yi6QnQRuC4qn1rh2eeIF9MHQL8G-D3k,26592
114
- lfx/components/agents/mcp_component.py,sha256=dW0eENDKz8esIShOooDEL48r3J3GoI1h0tuqIPLSnR4,25462
114
+ lfx/components/agents/mcp_component.py,sha256=mE2HvbHcdkuWWylxmaNNZobbtgBRktOOakeGwUYs7Qs,25586
115
115
  lfx/components/aiml/__init__.py,sha256=DNKB-HMFGFYmsdkON-s8557ttgBXVXADmS-BcuSQiIQ,1087
116
116
  lfx/components/aiml/aiml.py,sha256=23Ineg1ajlCoqXgWgp50I20OnQbaleRNsw1c6IzPu3A,3877
117
117
  lfx/components/aiml/aiml_embeddings.py,sha256=2uNwORuj55mxn2SfLbh7oAIfjuXwHbsnOqRjfMtQRqc,1095
@@ -400,7 +400,7 @@ lfx/components/notdiamond/notdiamond.py,sha256=om6_UB9n5rt1T-yXxgMFBPBEP2tJtnGC2
400
400
  lfx/components/novita/__init__.py,sha256=i8RrVPX00S3RupAlZ078-mdGB7VHwvpdnL7IfsWWPIo,937
401
401
  lfx/components/novita/novita.py,sha256=IULE3StkQwECxOR3HMJsEyE7cN5hwslxovvhMmquuNo,4368
402
402
  lfx/components/nvidia/__init__.py,sha256=Phf45VUW7An5LnauqpB-lIRVwwBiQawZkoWbqBjQnWE,1756
403
- lfx/components/nvidia/nvidia.py,sha256=Z5EomVSAzV_fqtgSKw4kO2ko59IIoXiQat8cgfpTlks,6180
403
+ lfx/components/nvidia/nvidia.py,sha256=Ej8WbpJD5fWAtFsMa4qUiK1Xu6igL6SQYwM0hk5mPGo,6116
404
404
  lfx/components/nvidia/nvidia_embedding.py,sha256=D97QOAgtZEzwHvBmDDShTmZhDAyN2SRbfb71515ib-g,2658
405
405
  lfx/components/nvidia/nvidia_ingest.py,sha256=_wxmYNmRQ2kBfAxaXLykBIlKFXVGXEsTY22spVeoCCI,12065
406
406
  lfx/components/nvidia/nvidia_rerank.py,sha256=zzl2skHxf2oXINDZBmG8-GbkTkc6EWtyMjyV8pVRAm4,2293
@@ -408,7 +408,7 @@ lfx/components/nvidia/system_assist.py,sha256=G8cgsLQxRBBnUt49_Uzxt7cdTNplVAzUlD
408
408
  lfx/components/olivya/__init__.py,sha256=ilZR88huL3vnQHO27g4jsUkyIYSgN7RPOq8Corbi6xA,67
409
409
  lfx/components/olivya/olivya.py,sha256=PDmsn8dBdSwAZUM2QGTyTwxGWsINCKaYR4yTjE-4lIQ,4192
410
410
  lfx/components/ollama/__init__.py,sha256=fau8QcWs_eHO2MmtQ4coiKj9CzFA9X4hqFf541ekgXk,1068
411
- lfx/components/ollama/ollama.py,sha256=eWSRR7lypH82Eb6px2JhPlJCfzDz33MCqgFoOXFbofE,13012
411
+ lfx/components/ollama/ollama.py,sha256=KSaBAdgyh1xmkIvVcBHRVwFwzBtR7wPkH4j3dlJnPC4,13946
412
412
  lfx/components/ollama/ollama_embeddings.py,sha256=nvg-JQvue6j7tcrbbPeq1U_-LUj1MKawWbXxnnvJlWM,3976
413
413
  lfx/components/openai/__init__.py,sha256=G4Fgw4pmmDohdIOmzaeSCGijzKjyqFXNJPLwlcUDZ3w,1113
414
414
  lfx/components/openai/openai.py,sha256=imWO1tTJ0tTLqax1v5bNBPCRINTj2f2wN8j5G-a07GI,4505
@@ -563,7 +563,7 @@ lfx/custom/code_parser/__init__.py,sha256=qIwZQdEp1z7ldn0z-GY44wmwRaywN3L6VPoPt6
563
563
  lfx/custom/code_parser/code_parser.py,sha256=QAqsp4QF607319dClK60BsaiwZLV55n0xeGR-DthSoE,14280
564
564
  lfx/custom/custom_component/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
565
565
  lfx/custom/custom_component/base_component.py,sha256=Pxi-qCocrGIwcG0x5fu-7ty1Py71bl_KG9Fku5SeO_M,4053
566
- lfx/custom/custom_component/component.py,sha256=MeYIPYab-68yarGX_IK0Fac8bGGPa1jlW5pv5P6IJ2E,74518
566
+ lfx/custom/custom_component/component.py,sha256=6BIJxp6M8y1wCVUTGoQveYVDzeMDnuqcItcJpua_UUk,75795
567
567
  lfx/custom/custom_component/component_with_cache.py,sha256=por6CiPL3EHdLp_DvfI7qz1n4tc1KkqMOJNbsxoqVaI,313
568
568
  lfx/custom/custom_component/custom_component.py,sha256=u330P-UbRXKeO-ughl2rCyvEbgdq-igTNFYEoKQLJzI,22306
569
569
  lfx/custom/directory_reader/__init__.py,sha256=eFjlhKjpt2Kha_sJ2EqWofLRbpvfOTjvDSCpdpaTqWk,77
@@ -612,7 +612,7 @@ lfx/inputs/input_mixin.py,sha256=r23bPxWb1Fo81kFU02a2KUGfPLNUhE6K9q1Zw4LH4Qw,108
612
612
  lfx/inputs/inputs.py,sha256=y-SwcZhlEuVgLsOa2_x7wraUCaoZ3EV7_jrON9yuqPw,26220
613
613
  lfx/inputs/validators.py,sha256=i_PyQHQUmNpeS-_jRJNNsP3WlTPMkCJk2iFmFt3_ijw,505
614
614
  lfx/interface/__init__.py,sha256=hlivcb8kMhU_V8VeXClNfz5fRyF-u5PZZMXkgu0U5a0,211
615
- lfx/interface/components.py,sha256=ud7F5wPoXkQDcqbDbf71uxHT5Bd6uNZVNzB3LTT8vLc,19896
615
+ lfx/interface/components.py,sha256=BotYhF246Ixm41AQb2aD5OJ7G8dIX_uE_55ZOrI4C70,20058
616
616
  lfx/interface/listing.py,sha256=fCpnp1F4foldTEbt1sQcF2HNqsaUZZ5uEyIe_FDL42c,711
617
617
  lfx/interface/run.py,sha256=m2u9r7K-v_FIe19GpwPUoaCeOMhA1iqp1k7Cy5G75uE,743
618
618
  lfx/interface/utils.py,sha256=qKi2HRg-QgeI3hmXLMtG6DHBbaQPuVMW5-o9lcN7j0Q,3790
@@ -644,8 +644,8 @@ lfx/schema/encoders.py,sha256=7vlWHZnZuDv1UVuP9X7Xn8srP1HZqLygOmkps3EJyY0,332
644
644
  lfx/schema/graph.py,sha256=o7qXhHZT4lEwjJZtlg4k9SNPgmMVZsZsclBbe8v_y6Y,1313
645
645
  lfx/schema/image.py,sha256=WdaOT3bjkJaG28RpgmurtfcnOG7Hr2phZ27YXH25uHA,5970
646
646
  lfx/schema/json_schema.py,sha256=UzMRSSAiLewJpf7B0XY4jPnPt0iskf61QUBxPdyiYys,6871
647
- lfx/schema/log.py,sha256=xbwSvJKmT1U8kxqIcV8BYgJxtu8Q6ntJKF8cIeksPEo,1943
648
- lfx/schema/message.py,sha256=Fg83OjS67qJYCevqt_5JcBu9xXkh1WY6S6DBLAvT62g,18076
647
+ lfx/schema/log.py,sha256=TISQa44D4pL_-AOw9p0nOPV-7s6Phl-0yrpuZihhEsU,1981
648
+ lfx/schema/message.py,sha256=mHHTX9OCHCGpA4goniQXF7I2UqEOy744ZFS4LMzNrYk,18261
649
649
  lfx/schema/openai_responses_schemas.py,sha256=drMCAlliefHfGRojBTMepPwk4DyEGh67naWvMPD10Sw,2596
650
650
  lfx/schema/properties.py,sha256=ZRY6FUDfqpc5wQ-bi-ZuUUrusF9t-pt9fQa_FNPpia0,1356
651
651
  lfx/schema/schema.py,sha256=XbIuvD64EdVljP1V32tsEL-ETXOQSFipMDaiMGzYttM,5079
@@ -712,14 +712,14 @@ lfx/utils/constants.py,sha256=4M8i93bROuQ7zmeKgfdNW85Znw7JFrK8KiagcDBpMRc,7036
712
712
  lfx/utils/data_structure.py,sha256=xU3JNa_4jcGOVa_ctfMxiImEj6dKQQPE_zZsTAyy2T4,6888
713
713
  lfx/utils/exceptions.py,sha256=RgIkI4uBssJsJUnuhluNGDSzdcuW5fnxPLhGfXYU9Uc,973
714
714
  lfx/utils/helpers.py,sha256=0LE0barnVp-8Y5cCoDRzhDzesvXqgiT7IXP6vtTSyGE,889
715
- lfx/utils/image.py,sha256=wMWBEI1gW3cFlQcio3mWgfHBaOw1uoAnqNmEacE_8xo,2133
715
+ lfx/utils/image.py,sha256=W9boQgz4WH3GOgLrYaRDz2CbX5Za8pzi044X3EKvYbI,2370
716
716
  lfx/utils/lazy_load.py,sha256=UDtXi8N7NT9r-FRGxsLUfDtGU_X8yqt-RQqgpc9TqAw,394
717
717
  lfx/utils/request_utils.py,sha256=A6vmwpr7f3ZUxHg6Sz2-BdUUsyAwg84-7N_DNoPC8_Q,518
718
718
  lfx/utils/schemas.py,sha256=NbOtVQBrn4d0BAu-0H_eCTZI2CXkKZlRY37XCSmuJwc,3865
719
719
  lfx/utils/util.py,sha256=Ww85wbr1-vjh2pXVtmTqoUVr6MXAW8S7eDx_Ys6HpE8,20696
720
720
  lfx/utils/util_strings.py,sha256=nU_IcdphNaj6bAPbjeL-c1cInQPfTBit8mp5Y57lwQk,1686
721
721
  lfx/utils/version.py,sha256=cHpbO0OJD2JQAvVaTH_6ibYeFbHJV0QDHs_YXXZ-bT8,671
722
- lfx_nightly-0.1.12.dev32.dist-info/METADATA,sha256=DPD5eQw6AfyD9J53ik2oQhNr-1XcmWuI63rddrAuD5k,8290
723
- lfx_nightly-0.1.12.dev32.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
724
- lfx_nightly-0.1.12.dev32.dist-info/entry_points.txt,sha256=1724p3RHDQRT2CKx_QRzEIa7sFuSVO0Ux70YfXfoMT4,42
725
- lfx_nightly-0.1.12.dev32.dist-info/RECORD,,
722
+ lfx_nightly-0.1.12.dev34.dist-info/METADATA,sha256=gNTSkSXCMVTO-NThZUB9QYis4SFNxIME5n0h_BBMJFY,8290
723
+ lfx_nightly-0.1.12.dev34.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
724
+ lfx_nightly-0.1.12.dev34.dist-info/entry_points.txt,sha256=1724p3RHDQRT2CKx_QRzEIa7sFuSVO0Ux70YfXfoMT4,42
725
+ lfx_nightly-0.1.12.dev34.dist-info/RECORD,,