griptape-nodes 0.48.0__py3-none-any.whl → 0.50.0__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.
@@ -32,7 +32,13 @@ with console.status("Loading Griptape Nodes...") as status:
32
32
  from griptape_nodes.retained_mode.managers.os_manager import OSManager
33
33
  from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
34
34
  from griptape_nodes.utils.uv_utils import find_uv_bin
35
- from griptape_nodes.utils.version_utils import get_complete_version_string, get_current_version, get_install_source
35
+ from griptape_nodes.utils.version_utils import (
36
+ get_complete_version_string,
37
+ get_current_version,
38
+ get_install_source,
39
+ get_latest_version_git,
40
+ get_latest_version_pypi,
41
+ )
36
42
 
37
43
  CONFIG_DIR = xdg_config_home() / "griptape_nodes"
38
44
  DATA_DIR = xdg_data_home() / "griptape_nodes"
@@ -642,38 +648,11 @@ def _get_latest_version(package: str, install_source: str) -> str:
642
648
  str: Latest release tag (e.g., "v0.31.4")
643
649
  """
644
650
  if install_source == "pypi":
645
- update_url = PYPI_UPDATE_URL.format(package=package)
646
-
647
- with httpx.Client() as client:
648
- response = client.get(update_url)
649
- try:
650
- response.raise_for_status()
651
- data = response.json()
652
- return f"v{data['info']['version']}"
653
- except httpx.HTTPStatusError as e:
654
- console.print(f"[red]Error fetching latest version: {e}[/red]")
655
- return get_current_version()
656
- elif install_source == "git":
657
- # We only install auto updating from the 'latest' tag
658
- revision = LATEST_TAG
659
- update_url = GITHUB_UPDATE_URL.format(package=package, revision=revision)
660
-
661
- with httpx.Client() as client:
662
- response = client.get(update_url)
663
- try:
664
- response.raise_for_status()
665
- # Get the latest commit SHA for the tag, this effectively the latest version of the package
666
- data = response.json()
667
- if "object" in data and "sha" in data["object"]:
668
- return data["object"]["sha"][:7]
669
- # Should not happen, but if it does, return the current version
670
- return get_current_version()
671
- except httpx.HTTPStatusError as e:
672
- console.print(f"[red]Error fetching latest version: {e}[/red]")
673
- return get_current_version()
674
- else:
675
- # If the package is installed from a file, just return the current version since the user is likely managing it manually
676
- return get_current_version()
651
+ return get_latest_version_pypi(package, PYPI_UPDATE_URL)
652
+ if install_source == "git":
653
+ return get_latest_version_git(package, GITHUB_UPDATE_URL, LATEST_TAG)
654
+ # If the package is installed from a file, just return the current version since the user is likely managing it manually
655
+ return get_current_version()
677
656
 
678
657
 
679
658
  def _auto_update_self() -> None:
griptape_nodes/app/app.py CHANGED
@@ -92,7 +92,6 @@ def start_app() -> None:
92
92
  Starts the event loop and listens for events from the Nodes API.
93
93
  """
94
94
  _init_event_listeners()
95
-
96
95
  # Listen for any signals to exit the app
97
96
  for sig in (signal.SIGINT, signal.SIGTERM):
98
97
  signal.signal(sig, lambda *_: sys.exit(0))
@@ -100,11 +99,9 @@ def start_app() -> None:
100
99
  api_key = _ensure_api_key()
101
100
  threading.Thread(target=mcp_server, args=(api_key,), daemon=True).start()
102
101
  threading.Thread(target=_listen_for_api_events, args=(api_key,), daemon=True).start()
103
-
104
102
  if STATIC_SERVER_ENABLED:
105
103
  static_dir = _build_static_dir()
106
104
  threading.Thread(target=start_api, args=(static_dir, event_queue), daemon=True).start()
107
-
108
105
  _process_event_queue()
109
106
 
110
107
 
@@ -263,8 +260,12 @@ def _process_event_queue() -> None:
263
260
  Event queue will be populated by background threads listening for events from the Nodes API.
264
261
  """
265
262
  # Wait for WebSocket connection to be established before processing events
266
- ws_ready_event.wait()
267
-
263
+ timed_out = ws_ready_event.wait(timeout=15)
264
+ if not timed_out:
265
+ console.print(
266
+ "[red] The connection to the websocket timed out. Please check your internet connection or the status of Griptape Nodes API.[/red]"
267
+ )
268
+ sys.exit(1)
268
269
  while True:
269
270
  event = event_queue.get(block=True)
270
271
  if isinstance(event, EventRequest):
@@ -92,7 +92,7 @@ class LocalWorkflowExecutor(WorkflowExecutor):
92
92
  node_name = result_event.payload.node_name
93
93
  flow_name = GriptapeNodes.NodeManager().get_node_parent_flow_by_name(node_name)
94
94
  event_request = EventRequest(request=SingleExecutionStepRequest(flow_name=flow_name))
95
- GriptapeNodes.handle_request(event_request.request)
95
+ self.queue.put(event_request)
96
96
 
97
97
  elif type(result_event.payload).__name__ == "NodeFinishProcessEvent":
98
98
  event_log = f"NodeFinishProcessEvent: {result_event.payload}"
@@ -196,7 +196,13 @@ class LocalWorkflowExecutor(WorkflowExecutor):
196
196
  try:
197
197
  event = self.queue.get(block=True)
198
198
 
199
- if isinstance(event, ExecutionGriptapeNodeEvent):
199
+ if isinstance(event, EventRequest):
200
+ # Handle EventRequest objects by processing them through GriptapeNodes
201
+ request_payload = event.request
202
+ GriptapeNodes.handle_request(
203
+ request_payload, response_topic=event.response_topic, request_id=event.request_id
204
+ )
205
+ elif isinstance(event, ExecutionGriptapeNodeEvent):
200
206
  result_event = event.wrapped_event
201
207
 
202
208
  if type(result_event.payload).__name__ == "ControlFlowResolvedEvent":
@@ -208,6 +214,8 @@ class LocalWorkflowExecutor(WorkflowExecutor):
208
214
  is_flow_finished = True
209
215
  logger.error(msg)
210
216
  error = LocalExecutorError(msg)
217
+ else:
218
+ logger.info("Unknown event type encountered: %s", type(event))
211
219
 
212
220
  self.queue.task_done()
213
221
 
@@ -714,6 +714,10 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
714
714
  # During save/load, this value IS still serialized to save its proper state.
715
715
  settable: bool = True
716
716
 
717
+ # "serializable" controls whether parameter values should be serialized during save/load operations.
718
+ # Set to False for parameters containing non-serializable types (ImageDrivers, PromptDrivers, file handles, etc.)
719
+ serializable: bool = True
720
+
717
721
  user_defined: bool = False
718
722
  _allowed_modes: set = field(
719
723
  default_factory=lambda: {
@@ -747,6 +751,7 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
747
751
  ui_options: dict | None = None,
748
752
  *,
749
753
  settable: bool = True,
754
+ serializable: bool = True,
750
755
  user_defined: bool = False,
751
756
  element_id: str | None = None,
752
757
  element_type: str | None = None,
@@ -764,6 +769,7 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
764
769
  self.tooltip_as_property = tooltip_as_property
765
770
  self.tooltip_as_output = tooltip_as_output
766
771
  self.settable = settable
772
+ self.serializable = serializable
767
773
  self.user_defined = user_defined
768
774
  if allowed_modes is None:
769
775
  self._allowed_modes = {ParameterMode.INPUT, ParameterMode.OUTPUT, ParameterMode.PROPERTY}
@@ -813,6 +819,8 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
813
819
  our_dict["tooltip_as_property"] = self.tooltip_as_property
814
820
 
815
821
  our_dict["is_user_defined"] = self.user_defined
822
+ our_dict["settable"] = self.settable
823
+ our_dict["serializable"] = self.serializable
816
824
  our_dict["ui_options"] = self.ui_options
817
825
 
818
826
  # Let's bundle up the mode details.
@@ -4,7 +4,7 @@ import logging
4
4
  from abc import ABC, abstractmethod
5
5
  from collections.abc import Callable, Generator, Iterable
6
6
  from enum import StrEnum, auto
7
- from typing import Any, TypeVar
7
+ from typing import Any, NamedTuple, TypeVar
8
8
 
9
9
  from griptape.events import BaseEvent, EventBus
10
10
 
@@ -26,12 +26,14 @@ from griptape_nodes.retained_mode.events.base_events import (
26
26
  ExecutionEvent,
27
27
  ExecutionGriptapeNodeEvent,
28
28
  ProgressEvent,
29
+ RequestPayload,
29
30
  )
30
31
  from griptape_nodes.retained_mode.events.execution_events import (
31
32
  NodeUnresolvedEvent,
32
33
  ParameterValueUpdateEvent,
33
34
  )
34
35
  from griptape_nodes.retained_mode.events.parameter_events import (
36
+ AddParameterToNodeRequest,
35
37
  RemoveElementEvent,
36
38
  RemoveParameterFromNodeRequest,
37
39
  )
@@ -52,6 +54,23 @@ class NodeResolutionState(StrEnum):
52
54
  RESOLVED = auto()
53
55
 
54
56
 
57
+ class NodeMessageResult(NamedTuple):
58
+ """Result from a node message callback.
59
+
60
+ Attributes:
61
+ success: True if the message was handled successfully, False otherwise
62
+ details: Human-readable description of what happened
63
+ response: Optional response data to return to the sender
64
+ altered_workflow_state: True if the message handling altered workflow state.
65
+ Clients can use this to determine if the workflow needs to be re-saved.
66
+ """
67
+
68
+ success: bool
69
+ details: str
70
+ response: Any = None
71
+ altered_workflow_state: bool = True
72
+
73
+
55
74
  class BaseNode(ABC):
56
75
  # Owned by a flow
57
76
  name: str
@@ -148,6 +167,15 @@ class BaseNode(ABC):
148
167
  """Callback to confirm allowing a Connection going OUT of this Node."""
149
168
  return True
150
169
 
170
+ def before_incoming_connection(
171
+ self,
172
+ source_node: BaseNode, # noqa: ARG002
173
+ source_parameter_name: str, # noqa: ARG002
174
+ target_parameter_name: str, # noqa: ARG002
175
+ ) -> None:
176
+ """Callback before validating a Connection coming TO this Node."""
177
+ return
178
+
151
179
  def after_incoming_connection(
152
180
  self,
153
181
  source_node: BaseNode, # noqa: ARG002
@@ -157,6 +185,15 @@ class BaseNode(ABC):
157
185
  """Callback after a Connection has been established TO this Node."""
158
186
  return
159
187
 
188
+ def before_outgoing_connection(
189
+ self,
190
+ source_parameter_name: str, # noqa: ARG002
191
+ target_node: BaseNode, # noqa: ARG002
192
+ target_parameter_name: str, # noqa: ARG002
193
+ ) -> None:
194
+ """Callback before validating a Connection going OUT of this Node."""
195
+ return
196
+
160
197
  def after_outgoing_connection(
161
198
  self,
162
199
  source_parameter: Parameter, # noqa: ARG002
@@ -251,6 +288,31 @@ class BaseNode(ABC):
251
288
  """Callback for when a Griptape Event comes destined for this Node."""
252
289
  return
253
290
 
291
+ def on_node_message_received(
292
+ self,
293
+ optional_element_name: str | None, # noqa: ARG002
294
+ message_type: str,
295
+ message: Any, # noqa: ARG002
296
+ ) -> NodeMessageResult:
297
+ """Callback for when a message is sent directly to this node.
298
+
299
+ Custom nodes may elect to override this method to handle specific message types
300
+ and implement custom communication patterns with external systems.
301
+
302
+ Args:
303
+ optional_element_name: Optional element name this message relates to
304
+ message_type: String indicating the message type for parsing
305
+ message: Message payload of any type
306
+
307
+ Returns:
308
+ NodeMessageResult: Result containing success status, details, and optional response
309
+ """
310
+ return NodeMessageResult(
311
+ success=False,
312
+ details=f"Node '{self.name}' was sent a message of type '{message_type}'. Failed because no message handler was specified for this node. Implement the on_node_message_received method in this node class in order for it to receive messages.",
313
+ response=None,
314
+ )
315
+
254
316
  def does_name_exist(self, param_name: str) -> bool:
255
317
  for parameter in self.parameters:
256
318
  if parameter.name == param_name:
@@ -1070,6 +1132,218 @@ class EndLoopNode(BaseNode):
1070
1132
  """Creating class for Start Loop Node in order to implement loop functionality in execution."""
1071
1133
 
1072
1134
 
1135
+ class ErrorProxyNode(BaseNode):
1136
+ """A proxy node that substitutes for nodes that failed to create due to missing dependencies or errors.
1137
+
1138
+ This node maintains the original node type information and allows workflows to continue loading
1139
+ even when some node types are unavailable. It generates parameters dynamically as connections
1140
+ and values are assigned to maintain workflow structure.
1141
+ """
1142
+
1143
+ def __init__(
1144
+ self,
1145
+ name: str,
1146
+ original_node_type: str,
1147
+ original_library_name: str,
1148
+ failure_reason: str,
1149
+ metadata: dict[Any, Any] | None = None,
1150
+ ) -> None:
1151
+ super().__init__(name, metadata)
1152
+
1153
+ self.original_node_type = original_node_type
1154
+ self.original_library_name = original_library_name
1155
+ self.failure_reason = failure_reason
1156
+ # Record ALL initial_setup=True requests in order for 1:1 replay
1157
+ self._recorded_initialization_requests: list[RequestPayload] = []
1158
+
1159
+ # Track if user has made connection modifications after initial setup
1160
+ self._has_connection_modifications: bool = False
1161
+
1162
+ # Add error message parameter explaining the failure
1163
+ self._error_message = ParameterMessage(
1164
+ name="error_proxy_message",
1165
+ variant="error",
1166
+ value="", # Will be set by _update_error_message
1167
+ )
1168
+ self.add_node_element(self._error_message)
1169
+ self._update_error_message()
1170
+
1171
+ def _get_base_error_message(self) -> str:
1172
+ """Generate the base error message for this ErrorProxyNode."""
1173
+ return (
1174
+ f"This is a placeholder for a node of type '{self.original_node_type}'"
1175
+ f"\nfrom the '{self.original_library_name}' library."
1176
+ f"\nIt encountered a problem when loading."
1177
+ f"\nThe technical issue:\n{self.failure_reason}\n\n"
1178
+ f"Your original node will be restored once the issue above is fixed"
1179
+ f"(which may require registering the appropriate library, or getting"
1180
+ f"a code fix from the node author)."
1181
+ )
1182
+
1183
+ def on_attempt_set_parameter_value(self, param_name: str) -> None:
1184
+ """Public method to attempt setting a parameter value during initial setup.
1185
+
1186
+ Creates a PROPERTY mode parameter if it doesn't exist to support value setting.
1187
+
1188
+ Args:
1189
+ param_name: Name of the parameter to prepare for value setting
1190
+ """
1191
+ self._ensure_parameter_exists(param_name)
1192
+
1193
+ def _ensure_parameter_exists(self, param_name: str) -> None:
1194
+ """Ensures a parameter exists on this node.
1195
+
1196
+ Creates a universal parameter with all modes enabled for maximum flexibility.
1197
+ Auto-generated parameters are marked as non-user-defined so they don't get serialized.
1198
+
1199
+ Args:
1200
+ param_name: Name of the parameter to ensure exists
1201
+ """
1202
+ existing_param = super().get_parameter_by_name(param_name)
1203
+
1204
+ if existing_param is None:
1205
+ # Create new universal parameter with all modes enabled
1206
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
1207
+
1208
+ request = AddParameterToNodeRequest(
1209
+ node_name=self.name,
1210
+ parameter_name=param_name,
1211
+ type=ParameterTypeBuiltin.ANY.value, # ANY = parameter's main type for maximum flexibility
1212
+ input_types=[ParameterTypeBuiltin.ANY.value], # ANY = accepts any single input type
1213
+ output_type=ParameterTypeBuiltin.ALL.value, # ALL = can output any type (passthrough)
1214
+ tooltip="Parameter created for placeholder node to preserve workflow connections",
1215
+ mode_allowed_input=True, # Enable all modes upfront
1216
+ mode_allowed_output=True,
1217
+ mode_allowed_property=True,
1218
+ is_user_defined=False, # Don't serialize this parameter
1219
+ initial_setup=True, # Allows setting non-settable parameters and prevents resolution cascades during workflow loading
1220
+ )
1221
+ result = GriptapeNodes.handle_request(request)
1222
+
1223
+ # Check if parameter creation was successful
1224
+ from griptape_nodes.retained_mode.events.parameter_events import AddParameterToNodeResultSuccess
1225
+
1226
+ if not isinstance(result, AddParameterToNodeResultSuccess):
1227
+ failure_message = f"Failed to create parameter '{param_name}': {result.result_details}"
1228
+ raise RuntimeError(failure_message)
1229
+ # If parameter already exists, nothing to do - it already has all modes
1230
+
1231
+ def allow_incoming_connection(
1232
+ self,
1233
+ source_node: BaseNode, # noqa: ARG002
1234
+ source_parameter: Parameter, # noqa: ARG002
1235
+ target_parameter: Parameter, # noqa: ARG002
1236
+ ) -> bool:
1237
+ """ErrorProxyNode allows connections - it's a shell for maintaining connections."""
1238
+ return True
1239
+
1240
+ def allow_outgoing_connection(
1241
+ self,
1242
+ source_parameter: Parameter, # noqa: ARG002
1243
+ target_node: BaseNode, # noqa: ARG002
1244
+ target_parameter: Parameter, # noqa: ARG002
1245
+ ) -> bool:
1246
+ """ErrorProxyNode allows connections - it's a shell for maintaining connections."""
1247
+ return True
1248
+
1249
+ def before_incoming_connection(
1250
+ self,
1251
+ source_node: BaseNode, # noqa: ARG002
1252
+ source_parameter_name: str, # noqa: ARG002
1253
+ target_parameter_name: str,
1254
+ ) -> None:
1255
+ """Create target parameter before connection validation."""
1256
+ self._ensure_parameter_exists(target_parameter_name)
1257
+
1258
+ def before_outgoing_connection(
1259
+ self,
1260
+ source_parameter_name: str,
1261
+ target_node: BaseNode, # noqa: ARG002
1262
+ target_parameter_name: str, # noqa: ARG002
1263
+ ) -> None:
1264
+ """Create source parameter before connection validation."""
1265
+ self._ensure_parameter_exists(source_parameter_name)
1266
+
1267
+ def set_post_init_connections_modified(self) -> None:
1268
+ """Mark that user-initiated connections have been modified and update the warning message."""
1269
+ if not self._has_connection_modifications:
1270
+ self._has_connection_modifications = True
1271
+ self._update_error_message()
1272
+
1273
+ def _update_error_message(self) -> None:
1274
+ """Update the ParameterMessage to include connection modification warning."""
1275
+ # Build the updated message with connection warning
1276
+ base_message = self._get_base_error_message()
1277
+
1278
+ # Add connection modification warning if applicable
1279
+ if self._has_connection_modifications:
1280
+ connection_warning = (
1281
+ "\n\nWARNING: You have modified connections to this placeholder node."
1282
+ "\nThis may require manual fixes when the original node is restored."
1283
+ )
1284
+ final_message = base_message + connection_warning
1285
+ else:
1286
+ # Add the general note only if no modifications have been made
1287
+ general_warning = (
1288
+ "\n\nNote: Making changes to this node may require manual fixes when restored,"
1289
+ "\nas we can't predict how all node authors craft their custom nodes."
1290
+ )
1291
+ final_message = base_message + general_warning
1292
+
1293
+ # Update the error message value
1294
+ self._error_message.value = final_message
1295
+
1296
+ def validate_before_node_run(self) -> list[Exception] | None:
1297
+ """Prevent ErrorProxy nodes from running - validate at node level only."""
1298
+ error_msg = (
1299
+ f"Cannot run node '{self.name}': This is a placeholder node put in place to preserve your workflow until the breaking issue is fixed.\n\n"
1300
+ f"The original '{self.original_node_type}' from library '{self.original_library_name}' failed to load due to this technical issue:\n\n"
1301
+ f"{self.failure_reason}\n\n"
1302
+ f"Once you resolve the issue above, reload this workflow and the placeholder will be automatically replaced with the original node."
1303
+ )
1304
+ return [RuntimeError(error_msg)]
1305
+
1306
+ def record_initialization_request(self, request: RequestPayload) -> None:
1307
+ """Record an initialization request for replay during serialization.
1308
+
1309
+ This method captures requests that modify ErrorProxyNode structure during workflow loading,
1310
+ preserving information needed for restoration when the original node becomes available.
1311
+
1312
+ WHAT WE RECORD:
1313
+ - AlterParameterDetailsRequest: Parameter modifications from original node definition
1314
+ - Any request with initial_setup=True that changes node structure in ways that cannot
1315
+ be reconstructed from final state alone
1316
+
1317
+ WHAT WE DO NOT RECORD (and why):
1318
+ - SetParameterValueRequest: Final parameter values are serialized normally via parameter_values
1319
+ - AddParameterToNodeRequest: User-defined parameters are serialized via is_user_defined=True flag
1320
+ - CreateConnectionRequest: Connections are serialized separately and recreated during loading
1321
+ - RenameParameterRequest: Final parameter names are preserved in serialized state
1322
+ - SetNodeMetadataRequest: Final metadata state is preserved in node.metadata
1323
+ - SetLockNodeStateRequest: Final lock state is preserved in node.lock
1324
+ """
1325
+ self._recorded_initialization_requests.append(request)
1326
+
1327
+ def get_recorded_initialization_requests(self, request_type: type | None = None) -> list[RequestPayload]:
1328
+ """Get recorded initialization requests for 1:1 serialization replay.
1329
+
1330
+ Args:
1331
+ request_type: Optional class to filter by. If provided, only returns requests
1332
+ of that type. If None, returns all recorded requests.
1333
+
1334
+ Returns:
1335
+ List of recorded requests in the order they were received.
1336
+ """
1337
+ if request_type is None:
1338
+ return self._recorded_initialization_requests
1339
+
1340
+ return [req for req in self._recorded_initialization_requests if isinstance(req, request_type)]
1341
+
1342
+ def process(self) -> Any:
1343
+ """No-op process method. Error Proxy nodes do nothing during execution."""
1344
+ return None
1345
+
1346
+
1073
1347
  class Connection:
1074
1348
  source_node: BaseNode
1075
1349
  target_node: BaseNode
@@ -45,6 +45,7 @@ class CreateNodeRequest(RequestPayload):
45
45
  resolution: Initial resolution state (defaults to UNRESOLVED)
46
46
  initial_setup: Skip setup work when loading from file (defaults to False)
47
47
  set_as_new_context: Set this node as current context after creation (defaults to False)
48
+ create_error_proxy_on_failure: Create Error Proxy node if creation fails (defaults to True)
48
49
 
49
50
  Results: CreateNodeResultSuccess (with assigned name) | CreateNodeResultFailure (invalid type, missing library, flow not found)
50
51
  """
@@ -60,6 +61,8 @@ class CreateNodeRequest(RequestPayload):
60
61
  initial_setup: bool = False
61
62
  # When True, this Node will be pushed as the current Node within the Current Context.
62
63
  set_as_new_context: bool = False
64
+ # When True, create an Error Proxy node if the requested node type fails to create
65
+ create_error_proxy_on_failure: bool = True
63
66
 
64
67
 
65
68
  @dataclass
@@ -677,3 +680,53 @@ class DuplicateSelectedNodesResultFailure(WorkflowNotAlteredMixin, ResultPayload
677
680
  Common causes: nodes not found, constraints/conflicts,
678
681
  insufficient resources, connection duplication failures.
679
682
  """
683
+
684
+
685
+ @dataclass
686
+ @PayloadRegistry.register
687
+ class SendNodeMessageRequest(RequestPayload):
688
+ """Send a message to a specific node.
689
+
690
+ Use when: External systems need to signal or send data directly to individual nodes,
691
+ implementing custom communication patterns, triggering node-specific behaviors.
692
+
693
+ Args:
694
+ node_name: Name of the target node (None for current context node)
695
+ optional_element_name: Optional element name this message relates to
696
+ message_type: String indicating message type for receiver parsing
697
+ message: Message payload of any type
698
+
699
+ Results: SendNodeMessageResultSuccess (with response) | SendNodeMessageResultFailure (node not found, handler error)
700
+ """
701
+
702
+ message_type: str
703
+ message: Any
704
+ node_name: str | None = None
705
+ optional_element_name: str | None = None
706
+
707
+
708
+ @dataclass
709
+ @PayloadRegistry.register
710
+ class SendNodeMessageResultSuccess(ResultPayloadSuccess):
711
+ """Node message sent and processed successfully.
712
+
713
+ Args:
714
+ response: Optional response data from the node's message handler
715
+ """
716
+
717
+ response: Any = None
718
+
719
+
720
+ @dataclass
721
+ @PayloadRegistry.register
722
+ class SendNodeMessageResultFailure(ResultPayloadFailure):
723
+ """Node message sending failed.
724
+
725
+ Common causes: node not found, no current context, message handler error,
726
+ unsupported message type.
727
+
728
+ Args:
729
+ response: Optional response data from the node's message handler (even on failure)
730
+ """
731
+
732
+ response: Any = None
@@ -40,6 +40,7 @@ class AddParameterToNodeRequest(RequestPayload):
40
40
  mode_allowed_input: Whether parameter can be used as input
41
41
  mode_allowed_property: Whether parameter can be used as property
42
42
  mode_allowed_output: Whether parameter can be used as output
43
+ is_user_defined: Whether this is a user-defined parameter (affects serialization)
43
44
  parent_container_name: Name of parent container if nested
44
45
  initial_setup: Skip setup work when loading from file
45
46
 
@@ -61,6 +62,7 @@ class AddParameterToNodeRequest(RequestPayload):
61
62
  mode_allowed_input: bool = Field(default=True)
62
63
  mode_allowed_property: bool = Field(default=True)
63
64
  mode_allowed_output: bool = Field(default=True)
65
+ is_user_defined: bool = Field(default=True)
64
66
  parent_container_name: str | None = None
65
67
  # initial_setup prevents unnecessary work when we are loading a workflow from a file.
66
68
  initial_setup: bool = False
@@ -14,7 +14,7 @@ from griptape_nodes.exe_types.core_types import (
14
14
  ParameterTypeBuiltin,
15
15
  )
16
16
  from griptape_nodes.exe_types.flow import ControlFlow
17
- from griptape_nodes.exe_types.node_types import BaseNode, NodeResolutionState, StartLoopNode, StartNode
17
+ from griptape_nodes.exe_types.node_types import BaseNode, ErrorProxyNode, NodeResolutionState, StartLoopNode, StartNode
18
18
  from griptape_nodes.machines.control_flow import CompleteState, ControlFlowMachine
19
19
  from griptape_nodes.retained_mode.events.base_events import (
20
20
  ExecutionEvent,
@@ -681,6 +681,18 @@ class FlowManager:
681
681
 
682
682
  # Cross-flow connections are now supported via global connection storage
683
683
 
684
+ # Call before_connection callbacks to allow nodes to prepare parameters
685
+ source_node.before_outgoing_connection(
686
+ source_parameter_name=request.source_parameter_name,
687
+ target_node=target_node,
688
+ target_parameter_name=request.target_parameter_name,
689
+ )
690
+ target_node.before_incoming_connection(
691
+ source_node=source_node,
692
+ source_parameter_name=request.source_parameter_name,
693
+ target_parameter_name=request.target_parameter_name,
694
+ )
695
+
684
696
  # Now validate the parameters.
685
697
  source_param = source_node.get_parameter_by_name(request.source_parameter_name)
686
698
  if source_param is None:
@@ -852,6 +864,13 @@ class FlowManager:
852
864
  )
853
865
  )
854
866
 
867
+ # Check if either node is ErrorProxyNode and mark connection modification if not initial_setup
868
+ if not request.initial_setup:
869
+ if isinstance(source_node, ErrorProxyNode):
870
+ source_node.set_post_init_connections_modified()
871
+ if isinstance(target_node, ErrorProxyNode):
872
+ target_node.set_post_init_connections_modified()
873
+
855
874
  result = CreateConnectionResultSuccess()
856
875
 
857
876
  return result
@@ -995,6 +1014,12 @@ class FlowManager:
995
1014
  details = f'Connection "{source_node_name}.{request.source_parameter_name}" to "{target_node_name}.{request.target_parameter_name}" deleted.'
996
1015
  logger.debug(details)
997
1016
 
1017
+ # Check if either node is ErrorProxyNode and mark connection modification (deletes are always user-initiated)
1018
+ if isinstance(source_node, ErrorProxyNode):
1019
+ source_node.set_post_init_connections_modified()
1020
+ if isinstance(target_node, ErrorProxyNode):
1021
+ target_node.set_post_init_connections_modified()
1022
+
998
1023
  result = DeleteConnectionResultSuccess()
999
1024
  return result
1000
1025
 
@@ -13,10 +13,17 @@ from griptape_nodes.exe_types.core_types import (
13
13
  ParameterTypeBuiltin,
14
14
  )
15
15
  from griptape_nodes.exe_types.flow import ControlFlow
16
- from griptape_nodes.exe_types.node_types import BaseNode, EndLoopNode, NodeResolutionState, StartLoopNode
16
+ from griptape_nodes.exe_types.node_types import (
17
+ BaseNode,
18
+ EndLoopNode,
19
+ ErrorProxyNode,
20
+ NodeResolutionState,
21
+ StartLoopNode,
22
+ )
17
23
  from griptape_nodes.exe_types.type_validator import TypeValidator
18
24
  from griptape_nodes.node_library.library_registry import LibraryNameAndVersion, LibraryRegistry
19
25
  from griptape_nodes.retained_mode.events.base_events import (
26
+ ResultDetails,
20
27
  ResultPayload,
21
28
  ResultPayloadFailure,
22
29
  )
@@ -73,6 +80,9 @@ from griptape_nodes.retained_mode.events.node_events import (
73
80
  ListParametersOnNodeRequest,
74
81
  ListParametersOnNodeResultFailure,
75
82
  ListParametersOnNodeResultSuccess,
83
+ SendNodeMessageRequest,
84
+ SendNodeMessageResultFailure,
85
+ SendNodeMessageResultSuccess,
76
86
  SerializedNodeCommands,
77
87
  SerializedParameterValueTracker,
78
88
  SerializedSelectedNodesCommands,
@@ -288,7 +298,30 @@ class NodeManager:
288
298
  traceback.print_exc()
289
299
  details = f"Could not create Node '{final_node_name}' of type '{request.node_type}': {err}"
290
300
  logger.error(details)
291
- return CreateNodeResultFailure(result_details=details)
301
+
302
+ # Check if we should create an Error Proxy node instead of failing
303
+ if request.create_error_proxy_on_failure:
304
+ try:
305
+ # Create ErrorProxyNode directly since it needs special initialization
306
+ node = ErrorProxyNode(
307
+ name=final_node_name,
308
+ original_node_type=request.node_type,
309
+ original_library_name=request.specific_library_name or "Unknown",
310
+ failure_reason=str(err),
311
+ metadata=request.metadata,
312
+ )
313
+
314
+ logger.warning(
315
+ "Created Error Proxy (placeholder) node '%s' to substitute for failed '%s'",
316
+ final_node_name,
317
+ request.node_type,
318
+ )
319
+ except Exception as proxy_err:
320
+ details = f"Failed to create Error Proxy (placeholder) node: {proxy_err}"
321
+ logger.error(details)
322
+ return CreateNodeResultFailure(result_details=details)
323
+ else:
324
+ return CreateNodeResultFailure(result_details=details)
292
325
  # Add it to the Flow.
293
326
  parent_flow.add_node(node)
294
327
 
@@ -873,7 +906,7 @@ class NodeManager:
873
906
  input_types=request.input_types,
874
907
  output_type=request.output_type,
875
908
  default_value=request.default_value,
876
- user_defined=True,
909
+ user_defined=request.is_user_defined,
877
910
  tooltip=request.tooltip,
878
911
  tooltip_as_input=request.tooltip_as_input,
879
912
  tooltip_as_property=request.tooltip_as_property,
@@ -1263,7 +1296,7 @@ class NodeManager:
1263
1296
 
1264
1297
  return None
1265
1298
 
1266
- def on_alter_parameter_details_request(self, request: AlterParameterDetailsRequest) -> ResultPayload: # noqa: C901, PLR0911
1299
+ def on_alter_parameter_details_request(self, request: AlterParameterDetailsRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912
1267
1300
  node_name = request.node_name
1268
1301
  node = None
1269
1302
 
@@ -1292,6 +1325,24 @@ class NodeManager:
1292
1325
  logger.error(details)
1293
1326
  return AlterParameterDetailsResultFailure(result_details=details)
1294
1327
 
1328
+ # Handle ErrorProxyNode parameter alteration requests
1329
+ if isinstance(node, ErrorProxyNode):
1330
+ if request.initial_setup:
1331
+ # Record the alteration request for serialization replay
1332
+ node.record_initialization_request(request)
1333
+
1334
+ # Early return with warning - we're just preserving the original changes
1335
+ details = f"Parameter '{request.parameter_name}' alteration recorded for ErrorProxyNode '{node_name}'. Original node '{node.original_node_type}' had loading errors - preserving changes for correct recreation when dependency '{node.original_library_name}' is resolved."
1336
+ logger.warning(details)
1337
+
1338
+ result_details = ResultDetails(message=details, level="WARNING")
1339
+ return AlterParameterDetailsResultSuccess(result_details=result_details)
1340
+
1341
+ # Reject runtime parameter alterations on ErrorProxy
1342
+ details = f"Cannot modify parameter '{request.parameter_name}' on placeholder node '{node_name}'. This placeholder preserves your workflow structure but doesn't allow parameter modifications, as they could cause issues when the original node is restored."
1343
+ logger.error(details)
1344
+ return AlterParameterDetailsResultFailure(result_details=details)
1345
+
1295
1346
  # Does the Element actually exist on the Node?
1296
1347
  element = node.get_element_by_name_and_type(request.parameter_name)
1297
1348
  if element is None:
@@ -1426,6 +1477,22 @@ class NodeManager:
1426
1477
  logger.error(details)
1427
1478
  return SetParameterValueResultFailure(result_details=details)
1428
1479
 
1480
+ # Handle ErrorProxyNode parameter value requests
1481
+ if isinstance(node, ErrorProxyNode):
1482
+ if request.initial_setup:
1483
+ # For initial_setup, actually create the parameter and set the value
1484
+ # This allows normal serialization to handle it, rather than recording the command
1485
+ node.on_attempt_set_parameter_value(param_name)
1486
+ # Continue with normal parameter value setting logic below
1487
+ logger.debug(
1488
+ "Created parameter '%s' on ErrorProxyNode '%s' during initial setup", param_name, node_name
1489
+ )
1490
+ else:
1491
+ # Reject runtime parameter value changes on ErrorProxy
1492
+ details = f"Cannot set parameter '{param_name}' on placeholder node '{node_name}'. This placeholder preserves your workflow structure but doesn't allow parameter changes, as they could cause issues when the original node is restored."
1493
+ logger.error(details)
1494
+ return SetParameterValueResultFailure(result_details=details)
1495
+
1429
1496
  # Does the Parameter actually exist on the Node?
1430
1497
  parameter = node.get_parameter_by_name(param_name)
1431
1498
  if parameter is None:
@@ -1885,20 +1952,42 @@ class NodeManager:
1885
1952
  library_used = node.metadata["library"]
1886
1953
  # Get the library metadata so we can get the version.
1887
1954
  library_metadata_request = GetLibraryMetadataRequest(library=library_used)
1888
- library_metadata_result = GriptapeNodes().handle_request(library_metadata_request)
1955
+ # Call LibraryManager directly to avoid error toasts when library is unavailable (expected for ErrorProxyNode)
1956
+ # Per https://github.com/griptape-ai/griptape-nodes/issues/1940
1957
+ library_metadata_result = GriptapeNodes.LibraryManager().get_library_metadata_request(
1958
+ library_metadata_request
1959
+ )
1960
+
1889
1961
  if not isinstance(library_metadata_result, GetLibraryMetadataResultSuccess):
1890
- details = f"Attempted to serialize Node '{node_name}' to commands. Failed to get metadata for library '{library_used}'."
1891
- logger.error(details)
1892
- return SerializeNodeToCommandsResultFailure(result_details=details)
1962
+ if isinstance(node, ErrorProxyNode):
1963
+ # For ErrorProxyNode, use descriptive message when original library unavailable
1964
+ library_version = "<version unavailable; workflow was saved when library was unable to be loaded>"
1965
+ library_details = LibraryNameAndVersion(library_name=library_used, library_version=library_version)
1966
+ details = f"Serializing Node '{node_name}' (original type: {node.original_node_type}) with unavailable library '{library_used}'. Saving as ErrorProxy with placeholder version. Fix the missing library and reload the workflow to restore the original node."
1967
+ logger.warning(details)
1968
+ else:
1969
+ # For regular nodes, this is still an error
1970
+ details = f"Attempted to serialize Node '{node_name}' to commands. Failed to get metadata for library '{library_used}'."
1971
+ logger.error(details)
1972
+ return SerializeNodeToCommandsResultFailure(result_details=details)
1973
+ else:
1974
+ library_version = library_metadata_result.metadata.library_version
1975
+ library_details = LibraryNameAndVersion(library_name=library_used, library_version=library_version)
1893
1976
 
1894
- library_version = library_metadata_result.metadata.library_version
1895
- library_details = LibraryNameAndVersion(library_name=library_used, library_version=library_version)
1977
+ # Handle ErrorProxyNode serialization - serialize as original node type
1978
+
1979
+ if isinstance(node, ErrorProxyNode):
1980
+ serialized_node_type = node.original_node_type
1981
+ serialized_library_name = node.original_library_name
1982
+ else:
1983
+ serialized_node_type = node.__class__.__name__
1984
+ serialized_library_name = library_details.library_name
1896
1985
 
1897
1986
  # Get the creation details.
1898
1987
  create_node_request = CreateNodeRequest(
1899
- node_type=node.__class__.__name__,
1988
+ node_type=serialized_node_type,
1900
1989
  node_name=node_name,
1901
- specific_library_name=library_details.library_name,
1990
+ specific_library_name=serialized_library_name,
1902
1991
  metadata=copy.deepcopy(node.metadata),
1903
1992
  # If it is actively resolving, mark as unresolved.
1904
1993
  resolution=node.state.value,
@@ -1906,20 +1995,42 @@ class NodeManager:
1906
1995
  )
1907
1996
 
1908
1997
  # We're going to compare this node instance vs. a canonical one. Rez that one up.
1909
- reference_node = type(node)(name="REFERENCE NODE")
1998
+ # For ErrorProxyNode, we can't create a reference node, so skip comparison
1999
+ if isinstance(node, ErrorProxyNode):
2000
+ reference_node = None
2001
+ else:
2002
+ reference_node = type(node)(name="REFERENCE NODE")
1910
2003
 
1911
2004
  # Now creation or alteration of all of the elements.
1912
2005
  element_modification_commands = []
1913
2006
  for parameter in node.parameters:
1914
2007
  # Create the parameter, or alter it on the existing node
1915
2008
  if parameter.user_defined:
1916
- # Add a user-defined Parameter.
2009
+ # Always serialize user-defined parameters regardless of node type
2010
+ param_dict = parameter.to_dict()
2011
+ param_dict["initial_setup"] = True
2012
+ add_param_request = AddParameterToNodeRequest.create(**param_dict)
2013
+ element_modification_commands.append(add_param_request)
2014
+ elif isinstance(node, ErrorProxyNode):
2015
+ # For ErrorProxyNode, replay all recorded initialization requests for this parameter
2016
+ recorded_requests = node.get_recorded_initialization_requests()
2017
+ matching_requests = [
2018
+ recorded_request
2019
+ for recorded_request in recorded_requests
2020
+ if (
2021
+ hasattr(recorded_request, "parameter_name")
2022
+ and getattr(recorded_request, "parameter_name", None) == parameter.name
2023
+ )
2024
+ ]
2025
+ element_modification_commands.extend(matching_requests)
2026
+ elif reference_node is None:
2027
+ # Normal node with no reference - treat all parameters as needing serialization
1917
2028
  param_dict = parameter.to_dict()
1918
2029
  param_dict["initial_setup"] = True
1919
2030
  add_param_request = AddParameterToNodeRequest.create(**param_dict)
1920
2031
  element_modification_commands.append(add_param_request)
1921
2032
  else:
1922
- # Not user defined. Get any deltas from the values on the reference node.
2033
+ # Normal node - compare against reference node
1923
2034
  diff = NodeManager._manage_alter_details(parameter, reference_node)
1924
2035
  relevant = False
1925
2036
  for key in diff:
@@ -1934,6 +2045,10 @@ class NodeManager:
1934
2045
 
1935
2046
  # Now assignment of values to all of the parameters.
1936
2047
  set_value_commands = []
2048
+
2049
+ # ErrorProxyNode uses normal parameter serialization now since we create real parameters
2050
+ # Only AlterParameterDetailsRequest commands are recorded and replayed
2051
+ # Normal node - use current parameter values
1937
2052
  for parameter in node.parameters:
1938
2053
  # SetParameterValueRequest event
1939
2054
  set_param_value_requests = NodeManager._handle_parameter_value_saving(
@@ -2298,6 +2413,7 @@ class NodeManager:
2298
2413
  value: Any,
2299
2414
  serialized_parameter_value_tracker: SerializedParameterValueTracker,
2300
2415
  unique_parameter_uuid_to_values: dict,
2416
+ parameter: Parameter,
2301
2417
  parameter_name: str,
2302
2418
  node_name: str,
2303
2419
  *,
@@ -2321,8 +2437,10 @@ class NodeManager:
2321
2437
  case SerializedParameterValueTracker.TrackerState.NOT_IN_TRACKER:
2322
2438
  # This value is new for us.
2323
2439
 
2324
- # Confirm that the author wants this parameter and/or class to be serialized.
2325
- # TODO: https://github.com/griptape-ai/griptape-nodes/issues/1179 ID a method for classes and/or parameters to be flagged for NOT serializability.
2440
+ # Check if parameter is marked as non-serializable (e.g., ImageDrivers, PromptDrivers, file handles)
2441
+ if not parameter.serializable:
2442
+ serialized_parameter_value_tracker.add_as_not_serializable(value_id)
2443
+ return None
2326
2444
 
2327
2445
  # Check if we can serialize it.
2328
2446
  try:
@@ -2403,12 +2521,13 @@ class NodeManager:
2403
2521
  value=internal_value,
2404
2522
  serialized_parameter_value_tracker=serialized_parameter_value_tracker,
2405
2523
  unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
2524
+ parameter=parameter,
2406
2525
  is_output=False,
2407
2526
  parameter_name=parameter.name,
2408
2527
  node_name=node.name,
2409
2528
  )
2410
2529
  if internal_command is None:
2411
- details = f"Attempted to serialize set value for parameter '{parameter.name}' on node '{node.name}'. The set value will not be restored in anything that attempts to deserialize or save this node. The value for this parameter was not serialized because it did not match Griptape Nodes' criteria for serializability. To remedy, either update the value's type to support serializability or mark the parameter as not serializable."
2530
+ details = f"Attempted to serialize set value for parameter '{parameter.name}' on node '{node.name}'. The set value will not be restored in anything that attempts to deserialize or save this node. The value for this parameter was not serialized because it did not match Griptape Nodes' criteria for serializability. To remedy, either update the value's type to support serializability or mark the parameter as not serializable by setting serializable=False when creating the parameter."
2412
2531
  logger.warning(details)
2413
2532
  else:
2414
2533
  commands.append(internal_command)
@@ -2417,12 +2536,13 @@ class NodeManager:
2417
2536
  value=output_value,
2418
2537
  serialized_parameter_value_tracker=serialized_parameter_value_tracker,
2419
2538
  unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
2539
+ parameter=parameter,
2420
2540
  is_output=True,
2421
2541
  parameter_name=parameter.name,
2422
2542
  node_name=node.name,
2423
2543
  )
2424
2544
  if output_command is None:
2425
- details = f"Attempted to serialize output value for parameter '{parameter.name}' on node '{node.name}'. The output value will not be restored in anything that attempts to deserialize or save this node. The value for this parameter was not serialized because it did not match Griptape Nodes' criteria for serializability. To remedy, either update the value's type to support serializability or mark the parameter as not serializable."
2545
+ details = f"Attempted to serialize output value for parameter '{parameter.name}' on node '{node.name}'. The output value will not be restored in anything that attempts to deserialize or save this node. The value for this parameter was not serialized because it did not match Griptape Nodes' criteria for serializability. To remedy, either update the value's type to support serializability or mark the parameter as not serializable by setting serializable=False when creating the parameter."
2426
2546
  logger.warning(details)
2427
2547
  else:
2428
2548
  commands.append(output_command)
@@ -2533,3 +2653,66 @@ class NodeManager:
2533
2653
  return SetLockNodeStateResultFailure(result_details=details)
2534
2654
  node.lock = request.lock
2535
2655
  return SetLockNodeStateResultSuccess(node_name=node_name, locked=node.lock)
2656
+
2657
+ def on_send_node_message_request(self, request: SendNodeMessageRequest) -> ResultPayload:
2658
+ """Handle a SendNodeMessageRequest by calling the node's message callback.
2659
+
2660
+ Args:
2661
+ request: The SendNodeMessageRequest containing message details
2662
+
2663
+ Returns:
2664
+ ResultPayload: Success or failure result with callback response
2665
+ """
2666
+ node_name = request.node_name
2667
+ node = None
2668
+
2669
+ if node_name is None:
2670
+ # Get from the current context
2671
+ if not GriptapeNodes.ContextManager().has_current_node():
2672
+ details = "Attempted to send message to Node from Current Context. Failed because the Current Context is empty."
2673
+ logger.error(details)
2674
+ return SendNodeMessageResultFailure(result_details=details)
2675
+
2676
+ node = GriptapeNodes.ContextManager().get_current_node()
2677
+ node_name = node.name
2678
+
2679
+ if node is None:
2680
+ # Find the node by name
2681
+ obj_mgr = GriptapeNodes.ObjectManager()
2682
+ node = obj_mgr.attempt_get_object_by_name_as_type(node_name, BaseNode)
2683
+ if node is None:
2684
+ details = f"Attempted to send message to Node '{node_name}', but no such Node was found."
2685
+ logger.error(details)
2686
+ return SendNodeMessageResultFailure(result_details=details)
2687
+
2688
+ # Validate optional_element_name if specified
2689
+ if request.optional_element_name is not None:
2690
+ element = node.root_ui_element.find_element_by_name(request.optional_element_name)
2691
+ if element is None:
2692
+ details = f"Attempted to send message to Node '{node_name}' with element '{request.optional_element_name}', but no such element was found."
2693
+ logger.error(details)
2694
+ return SendNodeMessageResultFailure(result_details=details, altered_workflow_state=False)
2695
+
2696
+ # Call the node's message callback
2697
+ callback_result = node.on_node_message_received(
2698
+ optional_element_name=request.optional_element_name,
2699
+ message_type=request.message_type,
2700
+ message=request.message,
2701
+ )
2702
+
2703
+ if not callback_result.success:
2704
+ details = f"Failed to handle message for Node '{node_name}': {callback_result.details}"
2705
+ logger.warning(details)
2706
+ return SendNodeMessageResultFailure(
2707
+ result_details=callback_result.details,
2708
+ response=callback_result.response,
2709
+ altered_workflow_state=callback_result.altered_workflow_state,
2710
+ )
2711
+
2712
+ details = f"Successfully sent message to Node '{node_name}': {callback_result.details}"
2713
+ logger.debug(details)
2714
+ return SendNodeMessageResultSuccess(
2715
+ result_details=callback_result.details,
2716
+ response=callback_result.response,
2717
+ altered_workflow_state=callback_result.altered_workflow_state,
2718
+ )
@@ -6,6 +6,11 @@ import importlib.metadata
6
6
  import json
7
7
  from typing import Literal
8
8
 
9
+ import httpx
10
+ from rich.console import Console
11
+
12
+ console = Console()
13
+
9
14
  engine_version = importlib.metadata.version("griptape_nodes")
10
15
 
11
16
 
@@ -49,3 +54,73 @@ def get_complete_version_string() -> str:
49
54
  if commit_id is None:
50
55
  return f"{version} ({source})"
51
56
  return f"{version} ({source} - {commit_id})"
57
+
58
+
59
+ def get_latest_version_pypi(package: str, pypi_url: str) -> str:
60
+ """Gets the latest version from PyPI.
61
+
62
+ Args:
63
+ package: The name of the package to fetch the latest version for.
64
+ pypi_url: The PyPI URL template to use.
65
+
66
+ Returns:
67
+ str: Latest release tag (e.g., "v0.31.4") or current version if fetch fails.
68
+ """
69
+ version = get_current_version()
70
+ update_url = pypi_url.format(package=package)
71
+
72
+ with httpx.Client(timeout=30.0) as client:
73
+ try:
74
+ response = client.get(update_url)
75
+ except httpx.RequestError as e:
76
+ console.print(f"[red]Error fetching latest version due to error: [/red][cyan]{e}[/cyan]")
77
+ console.print(
78
+ f"[red]Please check your internet connection or if you can access the following update url: [/red] [cyan]{update_url}[/cyan]"
79
+ )
80
+ return version
81
+
82
+ try:
83
+ response.raise_for_status()
84
+ data = response.json()
85
+ if "info" in data and "version" in data["info"]:
86
+ version = f"v{data['info']['version']}"
87
+ except httpx.HTTPStatusError as e:
88
+ console.print(f"[red]Error fetching latest version: {e}[/red]")
89
+
90
+ return version
91
+
92
+
93
+ def get_latest_version_git(package: str, github_url: str, latest_tag: str) -> str:
94
+ """Gets the latest version from Git.
95
+
96
+ Args:
97
+ package: The name of the package to fetch the latest version for.
98
+ github_url: The GitHub URL template to use.
99
+ latest_tag: The tag to fetch (usually 'latest').
100
+
101
+ Returns:
102
+ str: Latest commit SHA (first 7 characters) or current version if fetch fails.
103
+ """
104
+ version = get_current_version()
105
+ revision = latest_tag
106
+ update_url = github_url.format(package=package, revision=revision)
107
+
108
+ with httpx.Client(timeout=30.0) as client:
109
+ try:
110
+ response = client.get(update_url)
111
+ except httpx.RequestError as e:
112
+ console.print(f"[red]Error fetching latest version due to error: [/red][cyan]{e}[/cyan]")
113
+ console.print(
114
+ f"[red]Please check your internet connection or if you can access the following update url: [/red] [cyan]{update_url}[/cyan]"
115
+ )
116
+ return version
117
+
118
+ try:
119
+ response.raise_for_status()
120
+ data = response.json()
121
+ if "object" in data and "sha" in data["object"]:
122
+ version = data["object"]["sha"][:7]
123
+ except httpx.HTTPStatusError as e:
124
+ console.print(f"[red]Error fetching latest version: {e}[/red]")
125
+
126
+ return version
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: griptape-nodes
3
- Version: 0.48.0
3
+ Version: 0.50.0
4
4
  Summary: Add your description here
5
5
  Requires-Dist: griptape>=1.8.0
6
6
  Requires-Dist: pydantic>=2.10.6
@@ -1,12 +1,12 @@
1
- griptape_nodes/__init__.py,sha256=086f45e6ae694a24d828e6a1823376b75b6e64bf05ae95bb7e159bee199bd18e,38501
1
+ griptape_nodes/__init__.py,sha256=ca4ec8ef95679e0a98c8e75a539ee1a6eb00c8bf2d1d88bf0e44edb9b778e192,37367
2
2
  griptape_nodes/app/.python-version,sha256=7b55f8e67b5623c4bef3fa691288da9437d79d3aba156de48d481db32ac7d16d,5
3
3
  griptape_nodes/app/__init__.py,sha256=0c1f834ec81c3676e6102957128bb0cc686b9a8de01c10965c1f4190f1a97502,90
4
4
  griptape_nodes/app/api.py,sha256=46a36af068a4784b0317daede79befd59198320c3934ee1b84c74b297108774f,6925
5
- griptape_nodes/app/app.py,sha256=764f9ef01e11bd5649bc0b0b3e72c4a7e113b5c94eb2236770152cfd81a0eafa,17747
5
+ griptape_nodes/app/app.py,sha256=4d494cb0e1ba87c288b19a76c01015629c104fdf7a8668b6abf42e6e8baeaade,17984
6
6
  griptape_nodes/app/watch.py,sha256=413353c7811b54440276277250d766b3be30edf8eb8c128069dbb52d40e9e962,2064
7
7
  griptape_nodes/bootstrap/__init__.py,sha256=10dbf7488cd0f53b6546b835cb87b80a7a01a49685a454a43694c5055f17e4bb,25
8
8
  griptape_nodes/bootstrap/workflow_executors/__init__.py,sha256=a728cdf35f9e2ed8f21021582d73ad3028b18a223da818b9c9a95df3a88cec49,34
9
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py,sha256=c3711f000d42b3ea63558b574d327073fe8645f460337bd45cf1190a6a4113ba,8816
9
+ griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py,sha256=f09128692368e073d18cc602bcc0c81af8317eb4bfff486b30154262eb45f578,9274
10
10
  griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py,sha256=b610be5ba3b459d1f0f0ee6a01fa104bed225d6081ae1db42626de8c7bc6c90c,3347
11
11
  griptape_nodes/bootstrap/workflow_executors/workflow_executor.py,sha256=945000fb96d6cabd8a7f53324b9d2f5f549e1bb976ce66d2adbe7f932c8822ba,417
12
12
  griptape_nodes/drivers/__init__.py,sha256=b479a21509fab89c855610b63eba87811507ddddf20eca8c4295aa45ab266aa0,42
@@ -17,9 +17,9 @@ griptape_nodes/drivers/storage/local_storage_driver.py,sha256=3e912fb41fd35a0e5c
17
17
  griptape_nodes/drivers/storage/storage_backend.py,sha256=dd0048c2b80f73592b04888f60ba0adacc2120f0e22899e7b9c384772c7bcb77,184
18
18
  griptape_nodes/exe_types/__init__.py,sha256=c061b02865fdf9349aad415b5e90ef754fc9ede5c86d64c197f3422ce65af86f,32
19
19
  griptape_nodes/exe_types/connections.py,sha256=ffe54dcdfe8a6b2eb6433265497f8b9474d4873f82285f940ec6d92c9d25e969,12435
20
- griptape_nodes/exe_types/core_types.py,sha256=717894764822cad1cedc893adba82cac334a1070087a36fecaad3c9c3e583962,71043
20
+ griptape_nodes/exe_types/core_types.py,sha256=6cd742f545f89d7eb23aa6fdc10c8e706e035f459a57ae4977a630edee6b9e94,71471
21
21
  griptape_nodes/exe_types/flow.py,sha256=a42793af1f32ad480bc3553dc90c42edaaf3263c8fdaf6a0fa91b44132aeb7e3,5716
22
- griptape_nodes/exe_types/node_types.py,sha256=50ac21cb902277f5e3c42e83c8c16001a6fdbc4c8b6e09f62312204351fe5ec8,47447
22
+ griptape_nodes/exe_types/node_types.py,sha256=7c497246e4673e663e6ce3501bb632db5dbd2d2bc01c0117b064632cdce5f47d,59948
23
23
  griptape_nodes/exe_types/type_validator.py,sha256=453cf5bd7d3d8f34291e1a33213309d2c88e6545efb0444386b0298dd68cd563,1120
24
24
  griptape_nodes/machines/__init__.py,sha256=bf48e4c2bf8214e2e67c461442f5fc04a40b39dbe14ac03ab685e474770f1941,30
25
25
  griptape_nodes/machines/control_flow.py,sha256=ac7e9d4555eb0aa5c30cfa182d4df542e661f24b8871113bb26962de917451d5,9843
@@ -47,10 +47,10 @@ griptape_nodes/retained_mode/events/flow_events.py,sha256=084eda147bc1c98ea28b48
47
47
  griptape_nodes/retained_mode/events/generate_request_payload_schemas.py,sha256=3d0d104590140658d22068c1f1085851e4664ab7e62ec27a5d57328092a2d772,1102
48
48
  griptape_nodes/retained_mode/events/library_events.py,sha256=f4a54c1c0e556b36377d71089b7d599b2fab278dc0c4d8276b84b354f9b2f0f8,17629
49
49
  griptape_nodes/retained_mode/events/logger_events.py,sha256=8d8971ccfa26802b09b8fb49d339d60610fc4097c2f2a0b8c5f0a10e2b95bb28,705
50
- griptape_nodes/retained_mode/events/node_events.py,sha256=69b90367737ff7fb79f7976a9ff5e438e6bc414cf0d797d9c6481ed2a5e81ac5,25587
50
+ griptape_nodes/retained_mode/events/node_events.py,sha256=9402c9c59b07e3bc4f706361987441a82f9813954d7ab2b007ef671d3fe189dc,27298
51
51
  griptape_nodes/retained_mode/events/object_events.py,sha256=7096aa114ef72f37f5451c49ac5a84a65f1e4ebfa00e12a95cacb027ebd50894,2631
52
52
  griptape_nodes/retained_mode/events/os_events.py,sha256=5f49a6ca4febe6deaea5a43c7960a50fb276e40897da082c02ec4c6525bd571d,8085
53
- griptape_nodes/retained_mode/events/parameter_events.py,sha256=9f93c22ff4f9cf192d5f7852caaa43b1eced26b4fafd18d2f65bba0263410ead,18411
53
+ griptape_nodes/retained_mode/events/parameter_events.py,sha256=1607dd828ef4c3bdfef1ee22c3e8fcce33f432028d0e6a230e3a902cb85b9e86,18549
54
54
  griptape_nodes/retained_mode/events/payload_registry.py,sha256=abec3152aa7adef84d8fe3e5d491891e706485479f70202b3e830ddca0511042,1691
55
55
  griptape_nodes/retained_mode/events/secrets_events.py,sha256=7c2c3738c15fc41f054d9b38c9ba239b59ea36f85c8eda0dca54215fbe7e8e43,4156
56
56
  griptape_nodes/retained_mode/events/static_file_events.py,sha256=73c90a92bc403d9c6b13d7ec95da56b7078ad42e13fb5f9d1d6c3fefa4917a13,3764
@@ -65,7 +65,7 @@ griptape_nodes/retained_mode/managers/config_manager.py,sha256=d75b211dc72329f4b
65
65
  griptape_nodes/retained_mode/managers/context_manager.py,sha256=0e218d874f5367baec2c4717aaa07d62b87689eb41cb9f4b3c5ec6334381412f,22930
66
66
  griptape_nodes/retained_mode/managers/engine_identity_manager.py,sha256=181073ec72412553725cb191740bea0e08a131107f6235aa744c0b3029da7548,4409
67
67
  griptape_nodes/retained_mode/managers/event_manager.py,sha256=ac6a1e18ed004d977dc3c1a6d510c7a0e0c363227e5eac6218989d6228c7fd06,7201
68
- griptape_nodes/retained_mode/managers/flow_manager.py,sha256=d8c5e0a31f0613d2f49bb73a865d651b0d44332ff6ec306f0008090e787cc593,109060
68
+ griptape_nodes/retained_mode/managers/flow_manager.py,sha256=7922e81dc89ca57b7657a9ec9e84dd086165168fe03896e06b13bb7162ee2e16,110336
69
69
  griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py,sha256=22aefd5714dd113884c417c559771e26080366a4ba5f73a4cd713d5f015c955c,1340
70
70
  griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py,sha256=7ef00fd1569818f1d897f2a0dd12f881d265a6ac9af144efa4ff82e3dafde749,6723
71
71
  griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py,sha256=c44e88de3196c524a452db9979bc24d00bee9083f62732c1378b4daaafcc126d,14758
@@ -79,7 +79,7 @@ griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandb
79
79
  griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py,sha256=7021abf8c4354655418944d97a960429d5613e00b879b718ae6a6ff2b23755e3,894
80
80
  griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py,sha256=2b750407301d098f70a61c816cbc4360fd10e37698be12d9fcfcfbfd2cdc3de7,443
81
81
  griptape_nodes/retained_mode/managers/library_manager.py,sha256=c89cf240000f6bcf940a4e3106d968694787f51417ec5f90b1738c0852294f11,98816
82
- griptape_nodes/retained_mode/managers/node_manager.py,sha256=6710033aa69ae8d4a818b585264737ab14c68fffdb9f59225d889e418c818294,135551
82
+ griptape_nodes/retained_mode/managers/node_manager.py,sha256=1235d54ca8be9b84b59519984c9ac2fe946a51dc43e2659b36b6c1e376b32804,145538
83
83
  griptape_nodes/retained_mode/managers/object_manager.py,sha256=b483de02217de29b48a57c4cdbda858104da5dacd1817edf69324b57cef25991,12403
84
84
  griptape_nodes/retained_mode/managers/operation_manager.py,sha256=95390c66569a71382d579a15f9817a1c6f31b31f55fcbc17d5e834ef8593b369,20137
85
85
  griptape_nodes/retained_mode/managers/os_manager.py,sha256=6cf2580a42404a2b1b5ae03da832825d3aae18469538c76005358e3b5d1699b0,34347
@@ -113,7 +113,7 @@ griptape_nodes/utils/dict_utils.py,sha256=932962e4c39fcd2a2bd1af259977d80554bf35
113
113
  griptape_nodes/utils/image_preview.py,sha256=361608aa885117dbdb16958812f64774f2cb3cafc975406f0b846f340b126b5c,4480
114
114
  griptape_nodes/utils/metaclasses.py,sha256=4522b202f669a47fe16ccba0994988afc42a3a0d6c618e2e44d7d0c9b0f83bc4,287
115
115
  griptape_nodes/utils/uv_utils.py,sha256=64adfb2e669c29fc4c54a91c20ec89df91ce5e057ab05584f42ce028f99a454d,519
116
- griptape_nodes/utils/version_utils.py,sha256=f601cb158cd089036bec69ed7466d74e524e3dfdeaf1f6692024c7a38b001646,1649
116
+ griptape_nodes/utils/version_utils.py,sha256=63e1c97c95ba271bb3fe33a71f1322cbea6631ab8746763eec13b76a51a6c3e4,4318
117
117
  griptape_nodes/version_compatibility/__init__.py,sha256=24ccf5b01ed9b23ea94b54b9b1db0f474927f3db35034b01d24148bf280961fa,74
118
118
  griptape_nodes/version_compatibility/versions/__init__.py,sha256=3d64b1336f0b3d47417513c0f7c8f84545afb99e0da9f5f575acb6554e92a8fc,74
119
119
  griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py,sha256=db588a945a6c9815b85ac01ec520f458c0099ec62632adac21b0de602f37d5f7,62
@@ -121,7 +121,7 @@ griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_re
121
121
  griptape_nodes/version_compatibility/workflow_versions/__init__.py,sha256=cf95c38248b3a0d072097a72a37e217ec24a16c5a53876c3ea18337d81fdb9b7,52
122
122
  griptape_nodes/version_compatibility/workflow_versions/v0_7_0/__init__.py,sha256=2333cf9862bcea1dacc1f1864ce1f255c04894e9e0e928e0590ce56dfde7826a,51
123
123
  griptape_nodes/version_compatibility/workflow_versions/v0_7_0/local_executor_argument_addition.py,sha256=f4f725029fcc9b920fb5de728f95d24b9fb1ed062689fbe14e5cb6d02801666a,2018
124
- griptape_nodes-0.48.0.dist-info/WHEEL,sha256=a51d7af6bb8356f1000fb3d205aba975eca53e14c9e011c620094d822eb4863c,79
125
- griptape_nodes-0.48.0.dist-info/entry_points.txt,sha256=aaf7afa9ddc155b015e5371a9ed9c09b4a2ea9aa982a1b113b7a641729962d63,82
126
- griptape_nodes-0.48.0.dist-info/METADATA,sha256=62b03f4479b27f2548ff0dbbb1daf21749fca32f9aad00092d7968b1b24aebc1,4980
127
- griptape_nodes-0.48.0.dist-info/RECORD,,
124
+ griptape_nodes-0.50.0.dist-info/WHEEL,sha256=76443c98c0efcfdd1191eac5fa1d8223dba1c474dbd47676674a255e7ca48770,79
125
+ griptape_nodes-0.50.0.dist-info/entry_points.txt,sha256=aaf7afa9ddc155b015e5371a9ed9c09b4a2ea9aa982a1b113b7a641729962d63,82
126
+ griptape_nodes-0.50.0.dist-info/METADATA,sha256=461e5adb7bd3203d87800235d546f715d6467b8fb7f28916d95531544f51efcd,4980
127
+ griptape_nodes-0.50.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.8.10
2
+ Generator: uv 0.8.12
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any