griptape-nodes 0.43.1__py3-none-any.whl → 0.45.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.
Files changed (134) hide show
  1. griptape_nodes/__init__.py +46 -52
  2. griptape_nodes/app/.python-version +0 -0
  3. griptape_nodes/app/__init__.py +0 -0
  4. griptape_nodes/app/api.py +37 -41
  5. griptape_nodes/app/app.py +70 -3
  6. griptape_nodes/app/watch.py +5 -2
  7. griptape_nodes/bootstrap/__init__.py +0 -0
  8. griptape_nodes/bootstrap/workflow_executors/__init__.py +0 -0
  9. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +7 -1
  10. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +90 -0
  11. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +7 -1
  12. griptape_nodes/drivers/__init__.py +0 -0
  13. griptape_nodes/drivers/storage/__init__.py +0 -0
  14. griptape_nodes/drivers/storage/base_storage_driver.py +90 -0
  15. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +48 -0
  16. griptape_nodes/drivers/storage/local_storage_driver.py +37 -0
  17. griptape_nodes/drivers/storage/storage_backend.py +0 -0
  18. griptape_nodes/exe_types/__init__.py +0 -0
  19. griptape_nodes/exe_types/connections.py +0 -0
  20. griptape_nodes/exe_types/core_types.py +222 -17
  21. griptape_nodes/exe_types/flow.py +0 -0
  22. griptape_nodes/exe_types/node_types.py +20 -5
  23. griptape_nodes/exe_types/type_validator.py +0 -0
  24. griptape_nodes/machines/__init__.py +0 -0
  25. griptape_nodes/machines/control_flow.py +5 -4
  26. griptape_nodes/machines/fsm.py +0 -0
  27. griptape_nodes/machines/node_resolution.py +110 -74
  28. griptape_nodes/mcp_server/__init__.py +0 -0
  29. griptape_nodes/mcp_server/server.py +16 -8
  30. griptape_nodes/mcp_server/ws_request_manager.py +0 -0
  31. griptape_nodes/node_library/__init__.py +0 -0
  32. griptape_nodes/node_library/advanced_node_library.py +0 -0
  33. griptape_nodes/node_library/library_registry.py +0 -0
  34. griptape_nodes/node_library/workflow_registry.py +29 -0
  35. griptape_nodes/py.typed +0 -0
  36. griptape_nodes/retained_mode/__init__.py +0 -0
  37. griptape_nodes/retained_mode/events/__init__.py +0 -0
  38. griptape_nodes/retained_mode/events/agent_events.py +0 -0
  39. griptape_nodes/retained_mode/events/app_events.py +3 -8
  40. griptape_nodes/retained_mode/events/arbitrary_python_events.py +0 -0
  41. griptape_nodes/retained_mode/events/base_events.py +15 -7
  42. griptape_nodes/retained_mode/events/config_events.py +0 -0
  43. griptape_nodes/retained_mode/events/connection_events.py +0 -0
  44. griptape_nodes/retained_mode/events/context_events.py +0 -0
  45. griptape_nodes/retained_mode/events/execution_events.py +0 -0
  46. griptape_nodes/retained_mode/events/flow_events.py +2 -1
  47. griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +0 -0
  48. griptape_nodes/retained_mode/events/library_events.py +0 -0
  49. griptape_nodes/retained_mode/events/logger_events.py +0 -0
  50. griptape_nodes/retained_mode/events/node_events.py +36 -0
  51. griptape_nodes/retained_mode/events/object_events.py +0 -0
  52. griptape_nodes/retained_mode/events/os_events.py +98 -6
  53. griptape_nodes/retained_mode/events/parameter_events.py +0 -0
  54. griptape_nodes/retained_mode/events/payload_registry.py +0 -0
  55. griptape_nodes/retained_mode/events/secrets_events.py +0 -0
  56. griptape_nodes/retained_mode/events/static_file_events.py +0 -0
  57. griptape_nodes/retained_mode/events/sync_events.py +60 -0
  58. griptape_nodes/retained_mode/events/validation_events.py +0 -0
  59. griptape_nodes/retained_mode/events/workflow_events.py +231 -0
  60. griptape_nodes/retained_mode/griptape_nodes.py +9 -4
  61. griptape_nodes/retained_mode/managers/__init__.py +0 -0
  62. griptape_nodes/retained_mode/managers/agent_manager.py +0 -0
  63. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +0 -0
  64. griptape_nodes/retained_mode/managers/config_manager.py +1 -1
  65. griptape_nodes/retained_mode/managers/context_manager.py +0 -0
  66. griptape_nodes/retained_mode/managers/engine_identity_manager.py +0 -0
  67. griptape_nodes/retained_mode/managers/event_manager.py +0 -0
  68. griptape_nodes/retained_mode/managers/flow_manager.py +6 -0
  69. griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +0 -0
  70. griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +0 -0
  71. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +0 -0
  72. griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +0 -0
  73. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +0 -0
  74. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +0 -0
  75. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +0 -0
  76. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +0 -0
  77. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +0 -0
  78. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +0 -0
  79. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +0 -0
  80. griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +0 -0
  81. griptape_nodes/retained_mode/managers/library_manager.py +8 -26
  82. griptape_nodes/retained_mode/managers/node_manager.py +78 -7
  83. griptape_nodes/retained_mode/managers/object_manager.py +0 -0
  84. griptape_nodes/retained_mode/managers/operation_manager.py +7 -0
  85. griptape_nodes/retained_mode/managers/os_manager.py +133 -8
  86. griptape_nodes/retained_mode/managers/secrets_manager.py +0 -0
  87. griptape_nodes/retained_mode/managers/session_manager.py +0 -0
  88. griptape_nodes/retained_mode/managers/settings.py +5 -0
  89. griptape_nodes/retained_mode/managers/static_files_manager.py +0 -0
  90. griptape_nodes/retained_mode/managers/sync_manager.py +498 -0
  91. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +0 -0
  92. griptape_nodes/retained_mode/managers/workflow_manager.py +736 -33
  93. griptape_nodes/retained_mode/retained_mode.py +23 -0
  94. griptape_nodes/retained_mode/utils/__init__.py +0 -0
  95. griptape_nodes/retained_mode/utils/engine_identity.py +0 -0
  96. griptape_nodes/retained_mode/utils/name_generator.py +0 -0
  97. griptape_nodes/traits/__init__.py +0 -0
  98. griptape_nodes/traits/add_param_button.py +0 -0
  99. griptape_nodes/traits/button.py +0 -0
  100. griptape_nodes/traits/clamp.py +0 -0
  101. griptape_nodes/traits/compare.py +0 -0
  102. griptape_nodes/traits/compare_images.py +0 -0
  103. griptape_nodes/traits/file_system_picker.py +18 -0
  104. griptape_nodes/traits/minmax.py +0 -0
  105. griptape_nodes/traits/options.py +0 -0
  106. griptape_nodes/traits/slider.py +0 -0
  107. griptape_nodes/traits/trait_registry.py +0 -0
  108. griptape_nodes/traits/traits.json +0 -0
  109. griptape_nodes/updater/__init__.py +4 -2
  110. griptape_nodes/updater/__main__.py +0 -0
  111. griptape_nodes/utils/__init__.py +0 -0
  112. griptape_nodes/utils/dict_utils.py +0 -0
  113. griptape_nodes/utils/image_preview.py +0 -0
  114. griptape_nodes/utils/metaclasses.py +0 -0
  115. griptape_nodes/utils/uv_utils.py +18 -0
  116. griptape_nodes/utils/version_utils.py +51 -0
  117. griptape_nodes/version_compatibility/__init__.py +0 -0
  118. griptape_nodes/version_compatibility/versions/__init__.py +0 -0
  119. griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py +0 -0
  120. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +0 -0
  121. {griptape_nodes-0.43.1.dist-info → griptape_nodes-0.45.0.dist-info}/METADATA +2 -1
  122. {griptape_nodes-0.43.1.dist-info → griptape_nodes-0.45.0.dist-info}/RECORD +42 -47
  123. {griptape_nodes-0.43.1.dist-info → griptape_nodes-0.45.0.dist-info}/WHEEL +1 -1
  124. griptape_nodes/bootstrap/bootstrap_script.py +0 -54
  125. griptape_nodes/bootstrap/post_build_install_script.sh +0 -3
  126. griptape_nodes/bootstrap/pre_build_install_script.sh +0 -4
  127. griptape_nodes/bootstrap/register_libraries_script.py +0 -32
  128. griptape_nodes/bootstrap/structure_config.yaml +0 -15
  129. griptape_nodes/bootstrap/workflow_runners/__init__.py +0 -1
  130. griptape_nodes/bootstrap/workflow_runners/bootstrap_workflow_runner.py +0 -28
  131. griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +0 -237
  132. griptape_nodes/bootstrap/workflow_runners/subprocess_workflow_runner.py +0 -62
  133. griptape_nodes/bootstrap/workflow_runners/workflow_runner.py +0 -11
  134. {griptape_nodes-0.43.1.dist-info → griptape_nodes-0.45.0.dist-info}/entry_points.txt +0 -0
@@ -195,10 +195,56 @@ class ExecuteNodeState(State):
195
195
  EventBus.publish_event(ExecutionGriptapeNodeEvent(wrapped_event=ExecutionEvent(payload=payload)))
196
196
  current_node.parameter_output_values.clear()
197
197
 
198
+ @staticmethod
199
+ def collect_values_from_upstream_nodes(context: ResolutionContext) -> None:
200
+ """Collect output values from resolved upstream nodes and pass them to the current node.
201
+
202
+ This method iterates through all input parameters of the current node, finds their
203
+ connected upstream nodes, and if those nodes are resolved, retrieves their output
204
+ values and passes them through using SetParameterValueRequest.
205
+
206
+ Args:
207
+ context (ResolutionContext): The resolution context containing the focus stack
208
+ with the current node being processed.
209
+ """
210
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
211
+
212
+ current_node = context.focus_stack[-1].node
213
+ connections = GriptapeNodes.FlowManager().get_connections()
214
+
215
+ for parameter in current_node.parameters:
216
+ # Skip control type parameters
217
+ if ParameterTypeBuiltin.CONTROL_TYPE.value.lower() == parameter.output_type:
218
+ continue
219
+
220
+ # Get the connected upstream node for this parameter
221
+ upstream_connection = connections.get_connected_node(current_node, parameter)
222
+ if upstream_connection:
223
+ upstream_node, upstream_parameter = upstream_connection
224
+
225
+ # If the upstream node is resolved, collect its output value
226
+ if upstream_parameter.name in upstream_node.parameter_output_values:
227
+ output_value = upstream_node.parameter_output_values[upstream_parameter.name]
228
+
229
+ # Pass the value through using the same mechanism as normal resolution
230
+ GriptapeNodes.get_instance().handle_request(
231
+ SetParameterValueRequest(
232
+ parameter_name=parameter.name,
233
+ node_name=current_node.name,
234
+ value=output_value,
235
+ data_type=upstream_parameter.output_type,
236
+ )
237
+ )
238
+
198
239
  @staticmethod
199
240
  def on_enter(context: ResolutionContext) -> type[State] | None:
200
241
  current_node = context.focus_stack[-1].node
242
+
201
243
  # Clear all of the current output values
244
+ # if node is locked, don't clear anything. skip all of this.
245
+ if current_node.lock:
246
+ return ExecuteNodeState
247
+ ExecuteNodeState.collect_values_from_upstream_nodes(context)
202
248
  ExecuteNodeState.clear_parameter_output_values(context)
203
249
  for parameter in current_node.parameters:
204
250
  if ParameterTypeBuiltin.CONTROL_TYPE.value.lower() == parameter.output_type:
@@ -244,95 +290,85 @@ class ExecuteNodeState(State):
244
290
  # Once everything has been set
245
291
  current_focus = context.focus_stack[-1]
246
292
  current_node = current_focus.node
247
- # To set the event manager without circular import errors
248
- EventBus.publish_event(
249
- ExecutionGriptapeNodeEvent(
250
- wrapped_event=ExecutionEvent(payload=NodeStartProcessEvent(node_name=current_node.name))
251
- )
252
- )
253
- logger.info("Node '%s' is processing.", current_node.name)
254
-
255
- try:
256
- work_is_scheduled = ExecuteNodeState._process_node(current_focus)
257
- if work_is_scheduled:
258
- logger.debug("Pausing Node '%s' to run background work", current_node.name)
259
- return None
260
- except Exception as e:
261
- logger.exception("Error processing node '%s", current_node.name)
262
- msg = f"Canceling flow run. Node '{current_node.name}' encountered a problem: {e}"
263
- # Mark the node as unresolved, broadcasting to everyone.
264
- current_node.make_node_unresolved(
265
- current_states_to_trigger_change_event=set(
266
- {NodeResolutionState.UNRESOLVED, NodeResolutionState.RESOLVED, NodeResolutionState.RESOLVING}
267
- )
268
- )
269
- current_focus.process_generator = None
270
- current_focus.scheduled_value = None
271
-
272
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
273
-
274
- GriptapeNodes.FlowManager().cancel_flow_run()
275
-
293
+ # If the node is not locked, execute all of this.
294
+ if not current_node.lock:
295
+ # To set the event manager without circular import errors
276
296
  EventBus.publish_event(
277
297
  ExecutionGriptapeNodeEvent(
278
- wrapped_event=ExecutionEvent(payload=NodeFinishProcessEvent(node_name=current_node.name))
298
+ wrapped_event=ExecutionEvent(payload=NodeStartProcessEvent(node_name=current_node.name))
279
299
  )
280
300
  )
281
- raise RuntimeError(msg) from e
301
+ logger.info("Node '%s' is processing.", current_node.name)
282
302
 
283
- logger.info("Node '%s' finished processing.", current_node.name)
303
+ try:
304
+ work_is_scheduled = ExecuteNodeState._process_node(current_focus)
305
+ if work_is_scheduled:
306
+ logger.debug("Pausing Node '%s' to run background work", current_node.name)
307
+ return None
308
+ except Exception as e:
309
+ logger.exception("Error processing node '%s", current_node.name)
310
+ msg = f"Canceling flow run. Node '{current_node.name}' encountered a problem: {e}"
311
+ # Mark the node as unresolved, broadcasting to everyone.
312
+ current_node.make_node_unresolved(
313
+ current_states_to_trigger_change_event=set(
314
+ {NodeResolutionState.UNRESOLVED, NodeResolutionState.RESOLVED, NodeResolutionState.RESOLVING}
315
+ )
316
+ )
317
+ current_focus.process_generator = None
318
+ current_focus.scheduled_value = None
284
319
 
285
- EventBus.publish_event(
286
- ExecutionGriptapeNodeEvent(
287
- wrapped_event=ExecutionEvent(payload=NodeFinishProcessEvent(node_name=current_node.name))
288
- )
289
- )
290
- current_node.state = NodeResolutionState.RESOLVED
291
- details = f"'{current_node.name}' resolved."
320
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
292
321
 
293
- logger.info(details)
322
+ GriptapeNodes.FlowManager().cancel_flow_run()
294
323
 
295
- # Serialization can be slow so only do it if the user wants debug details.
296
- if logger.level <= logging.DEBUG:
297
- logger.debug(
298
- "INPUTS: %s\nOUTPUTS: %s",
299
- TypeValidator.safe_serialize(current_node.parameter_values),
300
- TypeValidator.safe_serialize(current_node.parameter_output_values),
301
- )
324
+ EventBus.publish_event(
325
+ ExecutionGriptapeNodeEvent(
326
+ wrapped_event=ExecutionEvent(payload=NodeFinishProcessEvent(node_name=current_node.name))
327
+ )
328
+ )
329
+ raise RuntimeError(msg) from e
330
+
331
+ logger.info("Node '%s' finished processing.", current_node.name)
302
332
 
303
- for parameter_name, value in current_node.parameter_output_values.items():
304
- parameter = current_node.get_parameter_by_name(parameter_name)
305
- if parameter is None:
306
- err = f"Canceling flow run. Node '{current_node.name}' specified a Parameter '{parameter_name}', but no such Parameter could be found on that Node."
307
- raise KeyError(err)
308
- data_type = parameter.type
309
- if data_type is None:
310
- data_type = ParameterTypeBuiltin.NONE.value
311
333
  EventBus.publish_event(
312
334
  ExecutionGriptapeNodeEvent(
313
- wrapped_event=ExecutionEvent(
314
- payload=ParameterValueUpdateEvent(
315
- node_name=current_node.name,
316
- parameter_name=parameter_name,
317
- data_type=data_type,
318
- value=TypeValidator.safe_serialize(value),
319
- )
320
- ),
335
+ wrapped_event=ExecutionEvent(payload=NodeFinishProcessEvent(node_name=current_node.name))
321
336
  )
322
337
  )
323
- # Pass the value through to the new nodes.
324
- conn_output_nodes = GriptapeNodes.FlowManager().get_connected_output_parameters(current_node, parameter)
325
- for target_node, target_parameter in conn_output_nodes:
326
- GriptapeNodes.get_instance().handle_request(
327
- SetParameterValueRequest(
328
- parameter_name=target_parameter.name,
329
- node_name=target_node.name,
330
- value=value,
331
- data_type=parameter.output_type,
332
- )
338
+ current_node.state = NodeResolutionState.RESOLVED
339
+ details = f"'{current_node.name}' resolved."
340
+
341
+ logger.info(details)
342
+
343
+ # Serialization can be slow so only do it if the user wants debug details.
344
+ if logger.level <= logging.DEBUG:
345
+ logger.debug(
346
+ "INPUTS: %s\nOUTPUTS: %s",
347
+ TypeValidator.safe_serialize(current_node.parameter_values),
348
+ TypeValidator.safe_serialize(current_node.parameter_output_values),
333
349
  )
334
350
 
335
- # Output values should already be saved!
351
+ for parameter_name, value in current_node.parameter_output_values.items():
352
+ parameter = current_node.get_parameter_by_name(parameter_name)
353
+ if parameter is None:
354
+ err = f"Canceling flow run. Node '{current_node.name}' specified a Parameter '{parameter_name}', but no such Parameter could be found on that Node."
355
+ raise KeyError(err)
356
+ data_type = parameter.type
357
+ if data_type is None:
358
+ data_type = ParameterTypeBuiltin.NONE.value
359
+ EventBus.publish_event(
360
+ ExecutionGriptapeNodeEvent(
361
+ wrapped_event=ExecutionEvent(
362
+ payload=ParameterValueUpdateEvent(
363
+ node_name=current_node.name,
364
+ parameter_name=parameter_name,
365
+ data_type=data_type,
366
+ value=TypeValidator.safe_serialize(value),
367
+ )
368
+ ),
369
+ )
370
+ )
371
+ # Output values should already be saved!
336
372
  library = LibraryRegistry.get_libraries_with_node_type(current_node.__class__.__name__)
337
373
  if len(library) == 1:
338
374
  library_name = library[0]
File without changes
@@ -57,19 +57,21 @@ SUPPORTED_REQUEST_EVENTS: dict[str, type[RequestPayload]] = {
57
57
  "SetParameterValueRequest": SetParameterValueRequest,
58
58
  }
59
59
 
60
+ GTN_MCP_SERVER_HOST = os.getenv("GTN_MCP_SERVER_HOST", "localhost")
60
61
  GTN_MCP_SERVER_PORT = int(os.getenv("GTN_MCP_SERVER_PORT", "9927"))
62
+ GTN_MCP_SERVER_LOG_LEVEL = os.getenv("GTN_MCP_SERVER_LOG_LEVEL", "ERROR").lower()
61
63
 
62
64
  config_manager = ConfigManager()
63
65
  secrets_manager = SecretsManager(config_manager)
64
66
 
67
+ mcp_server_logger = logging.getLogger("griptape_nodes_mcp_server")
68
+ mcp_server_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
69
+ mcp_server_logger.setLevel(logging.INFO)
70
+
65
71
 
66
72
  def main(api_key: str) -> None:
67
73
  """Main entry point for the Griptape Nodes MCP server."""
68
- mcp_server_logger = logging.getLogger("griptape_nodes_mcp_server")
69
- mcp_server_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
70
- mcp_server_logger.setLevel(logging.INFO)
71
- mcp_server_logger.info("Starting MCP GTN server...")
72
-
74
+ mcp_server_logger.debug("Starting MCP GTN server...")
73
75
  # Give these a session ID
74
76
  connection_manager = WebSocketConnectionManager()
75
77
  request_manager = AsyncRequestManager(connection_manager, api_key)
@@ -108,11 +110,11 @@ def main(api_key: str) -> None:
108
110
  async def lifespan(_: FastAPI) -> AsyncIterator[None]:
109
111
  """Context manager for managing session manager lifecycle."""
110
112
  async with session_manager.run():
111
- mcp_server_logger.info("GTN MCP server started with StreamableHTTP session manager!")
113
+ mcp_server_logger.debug("GTN MCP server started with StreamableHTTP session manager!")
112
114
  try:
113
115
  yield
114
116
  finally:
115
- mcp_server_logger.info("GTN MCP server shutting down...")
117
+ mcp_server_logger.debug("GTN MCP server shutting down...")
116
118
 
117
119
  # Create an ASGI application using the transport
118
120
  mcp_server_app = FastAPI(lifespan=lifespan)
@@ -123,4 +125,10 @@ def main(api_key: str) -> None:
123
125
 
124
126
  mcp_server_app.mount("/mcp", app=handle_streamable_http)
125
127
 
126
- uvicorn.run(mcp_server_app, host="127.0.0.1", port=GTN_MCP_SERVER_PORT)
128
+ uvicorn.run(
129
+ mcp_server_app,
130
+ host=GTN_MCP_SERVER_HOST,
131
+ port=GTN_MCP_SERVER_PORT,
132
+ log_config=None,
133
+ log_level=GTN_MCP_SERVER_LOG_LEVEL,
134
+ )
File without changes
File without changes
File without changes
File without changes
@@ -26,6 +26,7 @@ class WorkflowMetadata(BaseModel):
26
26
  is_template: bool | None = False
27
27
  creation_date: datetime | None = Field(default=None)
28
28
  last_modified_date: datetime | None = Field(default=None)
29
+ branched_from: str | None = Field(default=None)
29
30
 
30
31
 
31
32
  class WorkflowRegistry(metaclass=SingletonMeta):
@@ -81,6 +82,16 @@ class WorkflowRegistry(metaclass=SingletonMeta):
81
82
  raise KeyError(msg)
82
83
  return instance._workflows.pop(name)
83
84
 
85
+ @classmethod
86
+ def get_branches_of_workflow(cls, workflow_name: str) -> list[str]:
87
+ """Get all workflows that are branches of the specified workflow."""
88
+ instance = cls()
89
+ branches = []
90
+ for name, workflow in instance._workflows.items():
91
+ if workflow.metadata.branched_from == workflow_name:
92
+ branches.append(name)
93
+ return branches
94
+
84
95
 
85
96
  class Workflow:
86
97
  """A workflow card to be ran."""
@@ -102,6 +113,23 @@ class Workflow:
102
113
  msg = f"File path '{complete_path}' does not exist."
103
114
  raise ValueError(msg)
104
115
 
116
+ @property
117
+ def is_synced(self) -> bool:
118
+ """Check if this workflow is in the synced workflows directory."""
119
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
120
+
121
+ config_mgr = GriptapeNodes.ConfigManager()
122
+ synced_directory = config_mgr.get_config_value("synced_workflows_directory")
123
+
124
+ # Get the full path to the synced workflows directory
125
+ synced_path = config_mgr.get_full_path(synced_directory)
126
+
127
+ # Get the complete file path for this workflow
128
+ complete_file_path = WorkflowRegistry.get_complete_file_path(self.file_path)
129
+
130
+ # Check if the workflow file is within the synced directory
131
+ return Path(complete_file_path).is_relative_to(synced_path)
132
+
105
133
  def get_workflow_metadata(self) -> dict:
106
134
  # Convert from the Pydantic schema.
107
135
  ret_val = {**self.metadata.model_dump()}
@@ -109,4 +137,5 @@ class Workflow:
109
137
  # The schema doesn't have the file path in it, because it is baked into the file itself.
110
138
  # Customers of this function need that, so let's stuff it in.
111
139
  ret_val["file_path"] = self.file_path
140
+ ret_val["is_synced"] = self.is_synced
112
141
  return ret_val
griptape_nodes/py.typed CHANGED
File without changes
File without changes
File without changes
File without changes
@@ -5,6 +5,7 @@ from griptape_nodes.retained_mode.events.base_events import (
5
5
  RequestPayload,
6
6
  ResultPayloadFailure,
7
7
  ResultPayloadSuccess,
8
+ SkipTheLineMixin,
8
9
  WorkflowNotAlteredMixin,
9
10
  )
10
11
  from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
@@ -18,15 +19,9 @@ class AppStartSessionRequest(RequestPayload):
18
19
  Use when: Initializing client connections, beginning new workflow sessions,
19
20
  setting up isolated execution environments, managing session state.
20
21
 
21
- Args:
22
- session_id: Specific session ID to use (None for auto-generated)
23
-
24
22
  Results: AppStartSessionResultSuccess (with session ID) | AppStartSessionResultFailure (session creation error)
25
23
  """
26
24
 
27
- # TODO: https://github.com/griptape-ai/griptape-nodes/issues/1600
28
- session_id: str | None = None
29
-
30
25
 
31
26
  @dataclass
32
27
  @PayloadRegistry.register
@@ -154,7 +149,7 @@ class AppEndSessionResultFailure(ResultPayloadFailure):
154
149
 
155
150
  @dataclass
156
151
  @PayloadRegistry.register
157
- class SessionHeartbeatRequest(RequestPayload):
152
+ class SessionHeartbeatRequest(RequestPayload, SkipTheLineMixin):
158
153
  """Request clients can use ensure the engine session is still active."""
159
154
 
160
155
 
@@ -172,7 +167,7 @@ class SessionHeartbeatResultFailure(ResultPayloadFailure):
172
167
 
173
168
  @dataclass
174
169
  @PayloadRegistry.register
175
- class EngineHeartbeatRequest(RequestPayload):
170
+ class EngineHeartbeatRequest(RequestPayload, SkipTheLineMixin):
176
171
  """Request clients can use to discover active engines and their status.
177
172
 
178
173
  Attributes:
@@ -10,7 +10,7 @@ from griptape.events import BaseEvent as GtBaseEvent
10
10
  from griptape.mixins.serializable_mixin import SerializableMixin
11
11
  from griptape.structures import Structure
12
12
  from griptape.tools import BaseTool
13
- from pydantic import BaseModel, Field
13
+ from pydantic import BaseModel, ConfigDict, Field
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  import builtins
@@ -62,6 +62,15 @@ class WorkflowNotAlteredMixin:
62
62
  altered_workflow_state: bool = field(default=False, init=False)
63
63
 
64
64
 
65
+ class SkipTheLineMixin:
66
+ """Mixin for events that should skip the event queue and be processed immediately.
67
+
68
+ Events that implement this mixin will be handled directly without being added
69
+ to the event queue, allowing for priority processing of critical events like
70
+ heartbeats or other time-sensitive operations.
71
+ """
72
+
73
+
65
74
  # Success result payload abstract base class
66
75
  @dataclass(kw_only=True)
67
76
  class ResultPayloadSuccess(ResultPayload, ABC):
@@ -118,16 +127,15 @@ class BaseEvent(BaseModel, ABC):
118
127
  session_id: str | None = Field(default_factory=lambda: BaseEvent._session_id)
119
128
 
120
129
  # Custom JSON encoder for the payload
121
- class Config:
122
- """Pydantic configuration for the BaseEvent class."""
123
-
124
- arbitrary_types_allowed = True
125
- json_encoders: ClassVar[dict] = {
130
+ model_config = ConfigDict(
131
+ arbitrary_types_allowed=True,
132
+ json_encoders={
126
133
  # Use to_dict() methods for Griptape objects
127
134
  BaseArtifact: lambda obj: obj.to_dict(),
128
135
  BaseTool: lambda obj: obj.to_dict(),
129
136
  Structure: lambda obj: obj.to_dict(),
130
- }
137
+ },
138
+ )
131
139
 
132
140
  def dict(self, *args, **kwargs) -> dict[str, Any]:
133
141
  """Override dict to handle payload serialization and add event_type."""
File without changes
File without changes
File without changes
File without changes
@@ -9,7 +9,7 @@ from griptape_nodes.retained_mode.events.base_events import (
9
9
  WorkflowAlteredMixin,
10
10
  WorkflowNotAlteredMixin,
11
11
  )
12
- from griptape_nodes.retained_mode.events.node_events import SerializedNodeCommands
12
+ from griptape_nodes.retained_mode.events.node_events import SerializedNodeCommands, SetLockNodeStateRequest
13
13
  from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
14
14
  from griptape_nodes.retained_mode.events.workflow_events import ImportWorkflowAsReferencedSubFlowRequest
15
15
 
@@ -229,6 +229,7 @@ class SerializedFlowCommands:
229
229
  set_parameter_value_commands: dict[
230
230
  SerializedNodeCommands.NodeUUID, list[SerializedNodeCommands.IndirectSetParameterValueCommand]
231
231
  ]
232
+ set_lock_commands_per_node: dict[SerializedNodeCommands.NodeUUID, SetLockNodeStateRequest]
232
233
  sub_flows_commands: list["SerializedFlowCommands"]
233
234
  referenced_workflows: set[str]
234
235
 
File without changes
File without changes
@@ -303,6 +303,7 @@ class GetAllNodeInfoResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess)
303
303
 
304
304
  metadata: dict
305
305
  node_resolution_state: str
306
+ locked: bool
306
307
  connections: ListConnectionsForNodeResultSuccess
307
308
  element_id_to_value: dict[str, ParameterInfoValue]
308
309
  root_node_element: dict[str, Any]
@@ -317,6 +318,39 @@ class GetAllNodeInfoResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure)
317
318
  """
318
319
 
319
320
 
321
+ @dataclass
322
+ @PayloadRegistry.register
323
+ class SetLockNodeStateRequest(WorkflowNotAlteredMixin, RequestPayload):
324
+ """Lock a node.
325
+
326
+ Use when: Implementing locking functionality, preventing changes to nodes.
327
+
328
+ Args:
329
+ node_name: Name of the node to lock
330
+ lock: Whether to lock or unlock the node. If true, the node will be locked, otherwise it will be unlocked.
331
+
332
+ Results: SetLockNodeStateResultSuccess (node locked) | SetLockNodeStateResultFailure (node not found)
333
+ """
334
+
335
+ node_name: str | None
336
+ lock: bool
337
+
338
+
339
+ @dataclass
340
+ @PayloadRegistry.register
341
+ class SetLockNodeStateResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
342
+ """Node locked successfully."""
343
+
344
+ node_name: str
345
+ locked: bool
346
+
347
+
348
+ @dataclass
349
+ @PayloadRegistry.register
350
+ class SetLockNodeStateResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
351
+ """Node failed to lock."""
352
+
353
+
320
354
  # A Node's state can be serialized to a sequence of commands that the engine runs.
321
355
  @dataclass
322
356
  class SerializedNodeCommands:
@@ -354,6 +388,7 @@ class SerializedNodeCommands:
354
388
  create_node_command: CreateNodeRequest
355
389
  element_modification_commands: list[RequestPayload]
356
390
  node_library_details: LibraryNameAndVersion
391
+ lock_node_command: SetLockNodeStateRequest | None = None
357
392
  node_uuid: NodeUUID = field(default_factory=lambda: SerializedNodeCommands.NodeUUID(str(uuid4())))
358
393
 
359
394
 
@@ -477,6 +512,7 @@ class SerializedSelectedNodesCommands:
477
512
  set_parameter_value_commands: dict[
478
513
  SerializedNodeCommands.NodeUUID, list[SerializedNodeCommands.IndirectSetParameterValueCommand]
479
514
  ]
515
+ set_lock_commands_per_node: dict[SerializedNodeCommands.NodeUUID, SetLockNodeStateRequest]
480
516
  serialized_connection_commands: list[IndirectConnectionSerialization]
481
517
 
482
518
 
File without changes