griptape-nodes 0.52.1__py3-none-any.whl → 0.54.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 (71) hide show
  1. griptape_nodes/__init__.py +8 -942
  2. griptape_nodes/__main__.py +6 -0
  3. griptape_nodes/app/app.py +48 -86
  4. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +35 -5
  5. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +15 -1
  6. griptape_nodes/cli/__init__.py +1 -0
  7. griptape_nodes/cli/commands/__init__.py +1 -0
  8. griptape_nodes/cli/commands/config.py +74 -0
  9. griptape_nodes/cli/commands/engine.py +80 -0
  10. griptape_nodes/cli/commands/init.py +550 -0
  11. griptape_nodes/cli/commands/libraries.py +96 -0
  12. griptape_nodes/cli/commands/models.py +504 -0
  13. griptape_nodes/cli/commands/self.py +120 -0
  14. griptape_nodes/cli/main.py +56 -0
  15. griptape_nodes/cli/shared.py +75 -0
  16. griptape_nodes/common/__init__.py +1 -0
  17. griptape_nodes/common/directed_graph.py +71 -0
  18. griptape_nodes/drivers/storage/base_storage_driver.py +40 -20
  19. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +24 -29
  20. griptape_nodes/drivers/storage/local_storage_driver.py +23 -14
  21. griptape_nodes/exe_types/core_types.py +60 -2
  22. griptape_nodes/exe_types/node_types.py +257 -38
  23. griptape_nodes/exe_types/param_components/__init__.py +1 -0
  24. griptape_nodes/exe_types/param_components/execution_status_component.py +138 -0
  25. griptape_nodes/machines/control_flow.py +195 -94
  26. griptape_nodes/machines/dag_builder.py +207 -0
  27. griptape_nodes/machines/fsm.py +10 -1
  28. griptape_nodes/machines/parallel_resolution.py +558 -0
  29. griptape_nodes/machines/{node_resolution.py → sequential_resolution.py} +30 -57
  30. griptape_nodes/node_library/library_registry.py +34 -1
  31. griptape_nodes/retained_mode/events/app_events.py +5 -1
  32. griptape_nodes/retained_mode/events/base_events.py +9 -9
  33. griptape_nodes/retained_mode/events/config_events.py +30 -0
  34. griptape_nodes/retained_mode/events/execution_events.py +2 -2
  35. griptape_nodes/retained_mode/events/model_events.py +296 -0
  36. griptape_nodes/retained_mode/events/node_events.py +4 -3
  37. griptape_nodes/retained_mode/griptape_nodes.py +34 -12
  38. griptape_nodes/retained_mode/managers/agent_manager.py +23 -5
  39. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +3 -1
  40. griptape_nodes/retained_mode/managers/config_manager.py +44 -3
  41. griptape_nodes/retained_mode/managers/context_manager.py +6 -5
  42. griptape_nodes/retained_mode/managers/event_manager.py +8 -2
  43. griptape_nodes/retained_mode/managers/flow_manager.py +150 -206
  44. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +1 -1
  45. griptape_nodes/retained_mode/managers/library_manager.py +35 -25
  46. griptape_nodes/retained_mode/managers/model_manager.py +1107 -0
  47. griptape_nodes/retained_mode/managers/node_manager.py +102 -220
  48. griptape_nodes/retained_mode/managers/object_manager.py +11 -5
  49. griptape_nodes/retained_mode/managers/os_manager.py +28 -13
  50. griptape_nodes/retained_mode/managers/secrets_manager.py +8 -4
  51. griptape_nodes/retained_mode/managers/settings.py +116 -7
  52. griptape_nodes/retained_mode/managers/static_files_manager.py +85 -12
  53. griptape_nodes/retained_mode/managers/sync_manager.py +17 -9
  54. griptape_nodes/retained_mode/managers/workflow_manager.py +186 -192
  55. griptape_nodes/retained_mode/retained_mode.py +19 -0
  56. griptape_nodes/servers/__init__.py +1 -0
  57. griptape_nodes/{mcp_server/server.py → servers/mcp.py} +1 -1
  58. griptape_nodes/{app/api.py → servers/static.py} +43 -40
  59. griptape_nodes/traits/add_param_button.py +1 -1
  60. griptape_nodes/traits/button.py +334 -6
  61. griptape_nodes/traits/color_picker.py +66 -0
  62. griptape_nodes/traits/multi_options.py +188 -0
  63. griptape_nodes/traits/numbers_selector.py +77 -0
  64. griptape_nodes/traits/options.py +93 -2
  65. griptape_nodes/traits/traits.json +4 -0
  66. griptape_nodes/utils/async_utils.py +31 -0
  67. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/METADATA +4 -1
  68. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/RECORD +71 -48
  69. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/WHEEL +1 -1
  70. /griptape_nodes/{mcp_server → servers}/ws_request_manager.py +0 -0
  71. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/entry_points.txt +0 -0
@@ -6,12 +6,13 @@ from abc import ABC, abstractmethod
6
6
  from collections.abc import Callable, Generator, Iterable
7
7
  from concurrent.futures import ThreadPoolExecutor
8
8
  from enum import StrEnum, auto
9
- from typing import Any, NamedTuple, TypeVar
9
+ from typing import TYPE_CHECKING, Any, TypeVar
10
10
 
11
11
  from griptape_nodes.exe_types.core_types import (
12
12
  BaseNodeElement,
13
13
  ControlParameterInput,
14
14
  ControlParameterOutput,
15
+ NodeMessageResult,
15
16
  Parameter,
16
17
  ParameterContainer,
17
18
  ParameterDictionary,
@@ -21,6 +22,7 @@ from griptape_nodes.exe_types.core_types import (
21
22
  ParameterMode,
22
23
  ParameterTypeBuiltin,
23
24
  )
25
+ from griptape_nodes.exe_types.param_components.execution_status_component import ExecutionStatusComponent
24
26
  from griptape_nodes.exe_types.type_validator import TypeValidator
25
27
  from griptape_nodes.retained_mode.events.base_events import (
26
28
  ExecutionEvent,
@@ -39,6 +41,9 @@ from griptape_nodes.retained_mode.events.parameter_events import (
39
41
  )
40
42
  from griptape_nodes.traits.options import Options
41
43
 
44
+ if TYPE_CHECKING:
45
+ from griptape_nodes.exe_types.core_types import NodeMessagePayload
46
+
42
47
  logger = logging.getLogger("griptape_nodes")
43
48
 
44
49
  T = TypeVar("T")
@@ -54,23 +59,6 @@ class NodeResolutionState(StrEnum):
54
59
  RESOLVED = auto()
55
60
 
56
61
 
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
-
74
62
  class BaseNode(ABC):
75
63
  # Owned by a flow
76
64
  name: str
@@ -80,7 +68,7 @@ class BaseNode(ABC):
80
68
  state: NodeResolutionState
81
69
  current_spotlight_parameter: Parameter | None = None
82
70
  parameter_values: dict[str, Any]
83
- parameter_output_values: dict[str, Any]
71
+ parameter_output_values: TrackedParameterOutputValues
84
72
  stop_flow: bool = False
85
73
  root_ui_element: BaseNodeElement
86
74
  _tracked_parameters: list[BaseNodeElement]
@@ -289,15 +277,18 @@ class BaseNode(ABC):
289
277
 
290
278
  def on_node_message_received(
291
279
  self,
292
- optional_element_name: str | None, # noqa: ARG002
280
+ optional_element_name: str | None,
293
281
  message_type: str,
294
- message: Any, # noqa: ARG002
282
+ message: NodeMessagePayload | None,
295
283
  ) -> NodeMessageResult:
296
284
  """Callback for when a message is sent directly to this node.
297
285
 
298
286
  Custom nodes may elect to override this method to handle specific message types
299
287
  and implement custom communication patterns with external systems.
300
288
 
289
+ If optional_element_name is provided, this method will attempt to find the
290
+ element and delegate the message handling to that element's on_message_received method.
291
+
301
292
  Args:
302
293
  optional_element_name: Optional element name this message relates to
303
294
  message_type: String indicating the message type for parsing
@@ -306,6 +297,26 @@ class BaseNode(ABC):
306
297
  Returns:
307
298
  NodeMessageResult: Result containing success status, details, and optional response
308
299
  """
300
+ # If optional_element_name is provided, delegate to the specific element
301
+ if optional_element_name is not None:
302
+ element = self.root_ui_element.find_element_by_name(optional_element_name)
303
+ if element is None:
304
+ return NodeMessageResult(
305
+ success=False,
306
+ details=f"Node '{self.name}' received message for element '{optional_element_name}' but no element with that name was found",
307
+ response=None,
308
+ )
309
+ # Delegate to the element's message handler
310
+ result = element.on_message_received(message_type, message)
311
+ if result is None:
312
+ return NodeMessageResult(
313
+ success=False,
314
+ details=f"Element '{optional_element_name}' received message type '{message_type}' but no handler was available",
315
+ response=None,
316
+ )
317
+ return result
318
+
319
+ # If no element name specified, fall back to node-level handling
309
320
  return NodeMessageResult(
310
321
  success=False,
311
322
  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.",
@@ -620,10 +631,10 @@ class BaseNode(ABC):
620
631
  # Allow custom node logic to prepare and possibly mutate the value before it is actually set.
621
632
  # Record any parameters modified for cascading.
622
633
  if not initial_setup:
623
- if not skip_before_value_set:
624
- final_value = self.before_value_set(parameter=parameter, value=candidate_value)
625
- else:
634
+ if skip_before_value_set:
626
635
  final_value = candidate_value
636
+ else:
637
+ final_value = self.before_value_set(parameter=parameter, value=candidate_value)
627
638
  # ACTUALLY SET THE NEW VALUE
628
639
  self.parameter_values[param_name] = final_value
629
640
 
@@ -705,6 +716,9 @@ class BaseNode(ABC):
705
716
  raise KeyError(err)
706
717
 
707
718
  def get_next_control_output(self) -> Parameter | None:
719
+ # The default behavior for nodes is to find the first control output found.
720
+ # Advanced nodes can override this behavior (e.g., nodes that have multiple possible
721
+ # control paths).
708
722
  for param in self.parameters:
709
723
  if (
710
724
  ParameterTypeBuiltin.CONTROL_TYPE.value == param.output_type
@@ -1097,6 +1111,10 @@ class TrackedParameterOutputValues(dict[str, Any]):
1097
1111
  for key in keys_to_clear:
1098
1112
  self._emit_parameter_change_event(key, None, deleted=True)
1099
1113
 
1114
+ def silent_clear(self) -> None:
1115
+ """Clear all values without emitting parameter change events."""
1116
+ super().clear()
1117
+
1100
1118
  def update(self, *args, **kwargs) -> None:
1101
1119
  # Handle both dict.update(other) and dict.update(**kwargs) patterns
1102
1120
  if args:
@@ -1136,28 +1154,185 @@ class TrackedParameterOutputValues(dict[str, Any]):
1136
1154
 
1137
1155
  class ControlNode(BaseNode):
1138
1156
  # Control Nodes may have one Control Input Port and at least one Control Output Port
1157
+ def __init__(
1158
+ self,
1159
+ name: str,
1160
+ metadata: dict[Any, Any] | None = None,
1161
+ input_control_name: str | None = None,
1162
+ output_control_name: str | None = None,
1163
+ ) -> None:
1164
+ super().__init__(name, metadata=metadata)
1165
+ self.control_parameter_in = ControlParameterInput(
1166
+ display_name=input_control_name if input_control_name is not None else "Flow In"
1167
+ )
1168
+ self.control_parameter_out = ControlParameterOutput(
1169
+ display_name=output_control_name if output_control_name is not None else "Flow Out"
1170
+ )
1171
+
1172
+ self.add_parameter(self.control_parameter_in)
1173
+ self.add_parameter(self.control_parameter_out)
1174
+
1175
+
1176
+ class DataNode(BaseNode):
1139
1177
  def __init__(self, name: str, metadata: dict[Any, Any] | None = None) -> None:
1140
1178
  super().__init__(name, metadata=metadata)
1141
- control_parameter_in = ControlParameterInput()
1142
- control_parameter_out = ControlParameterOutput()
1143
1179
 
1144
- self.add_parameter(control_parameter_in)
1145
- self.add_parameter(control_parameter_out)
1180
+ # Create control parameters like ControlNode, but initialize them as hidden
1181
+ # This allows the user to turn a DataNode "into" a Control Node; useful when
1182
+ # in situations like within a For Loop.
1183
+ self.control_parameter_in = ControlParameterInput()
1184
+ self.control_parameter_out = ControlParameterOutput()
1146
1185
 
1147
- def get_next_control_output(self) -> Parameter | None:
1148
- for param in self.parameters:
1149
- if (
1150
- ParameterTypeBuiltin.CONTROL_TYPE.value == param.output_type
1151
- and ParameterMode.OUTPUT in param.allowed_modes
1152
- ):
1153
- return param
1154
- return None
1186
+ # Hide the control parameters by default
1187
+ self.control_parameter_in.ui_options["hide"] = True
1188
+ self.control_parameter_out.ui_options["hide"] = True
1155
1189
 
1190
+ self.add_parameter(self.control_parameter_in)
1191
+ self.add_parameter(self.control_parameter_out)
1192
+
1193
+
1194
+ class SuccessFailureNode(BaseNode):
1195
+ """Base class for nodes that have success/failure branching with control outputs.
1196
+
1197
+ This class provides:
1198
+ - Control input parameter
1199
+ - Two control outputs: success ("exec_out") and failure ("failure")
1200
+ - Execution state tracking for control flow routing
1201
+ - Helper method to check outgoing connections
1202
+ - Helper method to create standard status output parameters
1203
+ """
1156
1204
 
1157
- class DataNode(BaseNode):
1158
1205
  def __init__(self, name: str, metadata: dict[Any, Any] | None = None) -> None:
1159
1206
  super().__init__(name, metadata=metadata)
1160
1207
 
1208
+ # Track execution state for control flow routing
1209
+ self._execution_succeeded: bool | None = None
1210
+
1211
+ # Add control input parameter
1212
+ self.control_parameter_in = ControlParameterInput()
1213
+ self.add_parameter(self.control_parameter_in)
1214
+
1215
+ # Add success control output (uses default "exec_out" name)
1216
+ self.control_parameter_out = ControlParameterOutput(
1217
+ display_name="Succeeded", tooltip="Control path when the operation succeeds"
1218
+ )
1219
+ self.add_parameter(self.control_parameter_out)
1220
+
1221
+ # Add failure control output
1222
+ self.failure_output = ControlParameterOutput(
1223
+ name="failure",
1224
+ display_name="Failed",
1225
+ tooltip="Control path when the operation fails",
1226
+ )
1227
+ self.add_parameter(self.failure_output)
1228
+
1229
+ def get_next_control_output(self) -> Parameter | None:
1230
+ """Determine which control output to follow based on execution result."""
1231
+ if self._execution_succeeded is None:
1232
+ # Execution hasn't completed yet
1233
+ self.stop_flow = True
1234
+ return None
1235
+
1236
+ if self._execution_succeeded:
1237
+ return self.control_parameter_out
1238
+ return self.failure_output
1239
+
1240
+ def _has_outgoing_connections(self, parameter: Parameter) -> bool:
1241
+ """Check if a specific parameter has outgoing connections."""
1242
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
1243
+
1244
+ connections = GriptapeNodes.FlowManager().get_connections()
1245
+
1246
+ # Check if node has any outgoing connections
1247
+ node_connections = connections.outgoing_index.get(self.name)
1248
+ if node_connections is None:
1249
+ return False
1250
+
1251
+ # Check if this specific parameter has any outgoing connections
1252
+ param_connections = node_connections.get(parameter.name, [])
1253
+ return len(param_connections) > 0
1254
+
1255
+ def _create_status_parameters(
1256
+ self,
1257
+ result_details_tooltip: str = "Details about the operation result",
1258
+ result_details_placeholder: str = "Details on the operation will be presented here.",
1259
+ ) -> None:
1260
+ """Create and add standard status output parameters in a collapsible group.
1261
+
1262
+ This method creates a "Status" ParameterGroup and immediately adds it to the node.
1263
+ Nodes that use this are responsible for calling this at their desired location
1264
+ in their class constructor.
1265
+
1266
+ Creates and adds:
1267
+ - was_successful: Boolean parameter indicating success/failure
1268
+ - result_details: String parameter with operation details
1269
+
1270
+ Args:
1271
+ result_details_tooltip: Custom tooltip for result_details parameter
1272
+ result_details_placeholder: Custom placeholder text for result_details parameter
1273
+ """
1274
+ # Create status component with OUTPUT modes for SuccessFailureNode
1275
+ self.status_component = ExecutionStatusComponent(
1276
+ self,
1277
+ was_successful_modes={ParameterMode.OUTPUT},
1278
+ result_details_modes={ParameterMode.OUTPUT},
1279
+ parameter_group_initially_collapsed=True,
1280
+ result_details_tooltip=result_details_tooltip,
1281
+ result_details_placeholder=result_details_placeholder,
1282
+ )
1283
+
1284
+ def _clear_execution_status(self) -> None:
1285
+ """Clear execution status and reset status parameters.
1286
+
1287
+ This method should be called at the start of process() to reset the node state.
1288
+ """
1289
+ self._execution_succeeded = None
1290
+ self.status_component.clear_execution_status("Beginning execution...")
1291
+
1292
+ def _set_status_results(self, *, was_successful: bool, result_details: str) -> None:
1293
+ """Set status results and update execution state.
1294
+
1295
+ This method should be called from the process() method to communicate success or failure.
1296
+ It sets the execution state for control flow routing and updates the status output parameters.
1297
+
1298
+ Args:
1299
+ was_successful: Whether the operation succeeded
1300
+ result_details: Details about the operation result
1301
+ """
1302
+ self._execution_succeeded = was_successful
1303
+ self.status_component.set_execution_result(was_successful=was_successful, result_details=result_details)
1304
+
1305
+ def _handle_failure_exception(self, exception: Exception) -> None:
1306
+ """Handle failure exceptions based on whether failure output is connected.
1307
+
1308
+ If the failure output has outgoing connections, logs the error and continues execution
1309
+ to allow graceful failure handling. If no connections exist, raises the exception
1310
+ to crash the flow and provide immediate feedback.
1311
+
1312
+ Args:
1313
+ exception: The exception that caused the failure
1314
+ """
1315
+ if self._has_outgoing_connections(self.failure_output):
1316
+ # User has connected something to Failed output, they want to handle errors gracefully
1317
+ logger.error(
1318
+ "Error in node '%s': %s. Continuing execution since failure output is connected for graceful handling.",
1319
+ self.name,
1320
+ exception,
1321
+ )
1322
+ else:
1323
+ # No graceful handling, raise the exception to crash the flow
1324
+ raise exception
1325
+
1326
+ def validate_before_workflow_run(self) -> list[Exception] | None:
1327
+ """Clear result details before workflow runs to avoid confusion from previous sessions."""
1328
+ self._set_status_results(was_successful=False, result_details="<Results will appear when the node executes>")
1329
+ return super().validate_before_workflow_run()
1330
+
1331
+ def validate_before_node_run(self) -> list[Exception] | None:
1332
+ """Clear result details before node runs to avoid confusion from previous sessions."""
1333
+ self._set_status_results(was_successful=False, result_details="<Results will appear when the node executes>")
1334
+ return super().validate_before_node_run()
1335
+
1161
1336
 
1162
1337
  class StartNode(BaseNode):
1163
1338
  def __init__(self, name: str, metadata: dict[Any, Any] | None = None) -> None:
@@ -1169,7 +1344,51 @@ class EndNode(BaseNode):
1169
1344
  # TODO: https://github.com/griptape-ai/griptape-nodes/issues/854
1170
1345
  def __init__(self, name: str, metadata: dict[Any, Any] | None = None) -> None:
1171
1346
  super().__init__(name, metadata)
1172
- self.add_parameter(ControlParameterInput())
1347
+
1348
+ # Add dual control inputs
1349
+ self.succeeded_control = ControlParameterInput(
1350
+ display_name="Succeeded", tooltip="Control path when the flow completed successfully"
1351
+ )
1352
+ self.failed_control = ControlParameterInput(
1353
+ name="failed", display_name="Failed", tooltip="Control path when the flow failed"
1354
+ )
1355
+
1356
+ self.add_parameter(self.succeeded_control)
1357
+ self.add_parameter(self.failed_control)
1358
+
1359
+ # Create status component with INPUT and PROPERTY modes
1360
+ self.status_component = ExecutionStatusComponent(
1361
+ self,
1362
+ was_successful_modes={ParameterMode.PROPERTY},
1363
+ result_details_modes={ParameterMode.INPUT},
1364
+ parameter_group_initially_collapsed=False,
1365
+ result_details_placeholder="Details about the completion or failure will be shown here.",
1366
+ )
1367
+
1368
+ def process(self) -> None:
1369
+ # Detect which control input was used to enter this node and determine success status
1370
+ match self._entry_control_parameter:
1371
+ case self.succeeded_control:
1372
+ was_successful = True
1373
+ status_prefix = "[SUCCEEDED]"
1374
+ case self.failed_control:
1375
+ was_successful = False
1376
+ status_prefix = "[FAILED]"
1377
+ case _:
1378
+ # No specific success/failure connection provided, assume success
1379
+ was_successful = True
1380
+ status_prefix = "[SUCCEEDED] No connection provided for success or failure, assuming successful"
1381
+
1382
+ # Get result details and format the final message
1383
+ result_details_value = self.get_parameter_value("result_details")
1384
+ if result_details_value and self._entry_control_parameter in (self.succeeded_control, self.failed_control):
1385
+ details = f"{status_prefix}\n{result_details_value}"
1386
+ elif self._entry_control_parameter in (self.succeeded_control, self.failed_control):
1387
+ details = f"{status_prefix}\nNo details supplied by flow"
1388
+ else:
1389
+ details = status_prefix
1390
+
1391
+ self.status_component.set_execution_result(was_successful=was_successful, result_details=details)
1173
1392
 
1174
1393
 
1175
1394
  class StartLoopNode(BaseNode):
@@ -0,0 +1 @@
1
+ """Parameter components for reusable node functionality."""
@@ -0,0 +1,138 @@
1
+ from typing import Any
2
+
3
+ from griptape_nodes.exe_types.core_types import (
4
+ Parameter,
5
+ ParameterGroup,
6
+ ParameterMode,
7
+ )
8
+
9
+
10
+ class ExecutionStatusComponent:
11
+ """A reusable component for managing execution status parameters.
12
+
13
+ This component creates and manages a "Status" ParameterGroup containing:
14
+ - was_successful: Boolean parameter indicating success/failure
15
+ - result_details: String parameter with operation details
16
+
17
+ The component can be customized for different parameter modes to support
18
+ various node types (EndNode uses INPUT/PROPERTY, SuccessFailureNode uses OUTPUT).
19
+ """
20
+
21
+ def __init__( # noqa: PLR0913
22
+ self,
23
+ node: Any, # BaseNode type, but avoiding circular import
24
+ *,
25
+ was_successful_modes: set[ParameterMode],
26
+ result_details_modes: set[ParameterMode],
27
+ parameter_group_initially_collapsed: bool = True,
28
+ result_details_tooltip: str = "Details about the operation result",
29
+ result_details_placeholder: str = "Details on the operation will be presented here.",
30
+ ) -> None:
31
+ """Initialize the ExecutionStatusComponent and create the parameters immediately.
32
+
33
+ Args:
34
+ node: The node instance that will own these parameters
35
+ was_successful_modes: Set of ParameterModes for was_successful parameter
36
+ result_details_modes: Set of ParameterModes for result_details parameter
37
+ parameter_group_initially_collapsed: Whether the Status group should start collapsed
38
+ result_details_tooltip: Custom tooltip for result_details parameter
39
+ result_details_placeholder: Custom placeholder text for result_details parameter
40
+ """
41
+ self._node = node
42
+
43
+ # Create the Status ParameterGroup
44
+ self._status_group = ParameterGroup(name="Status")
45
+ self._status_group.ui_options = {"collapsed": parameter_group_initially_collapsed}
46
+
47
+ # Boolean parameter to indicate success/failure
48
+ self._was_successful = Parameter(
49
+ name="was_successful",
50
+ tooltip="Indicates whether it completed without errors.",
51
+ type="bool",
52
+ default_value=False,
53
+ settable=False,
54
+ allowed_modes=was_successful_modes,
55
+ )
56
+
57
+ # Result details parameter with multiline option
58
+ self._result_details = Parameter(
59
+ name="result_details",
60
+ tooltip=result_details_tooltip,
61
+ type="str",
62
+ default_value=None,
63
+ allowed_modes=result_details_modes,
64
+ settable=False,
65
+ ui_options={
66
+ "multiline": True,
67
+ "placeholder_text": result_details_placeholder,
68
+ },
69
+ )
70
+
71
+ # Add parameters to the group
72
+ self._status_group.add_child(self._was_successful)
73
+ self._status_group.add_child(self._result_details)
74
+
75
+ # Add the group to the node
76
+ self._node.add_node_element(self._status_group)
77
+
78
+ def get_parameter_group(self) -> ParameterGroup:
79
+ """Get the Status ParameterGroup.
80
+
81
+ Returns:
82
+ ParameterGroup: The Status group containing was_successful and result_details
83
+ """
84
+ return self._status_group
85
+
86
+ def set_execution_result(self, *, was_successful: bool, result_details: str) -> None:
87
+ """Set the execution result values.
88
+
89
+ Args:
90
+ was_successful: Whether the operation succeeded
91
+ result_details: Details about the operation result
92
+ """
93
+ self._update_parameter_value(self._was_successful, was_successful)
94
+ self._update_parameter_value(self._result_details, result_details)
95
+
96
+ def clear_execution_status(self, initial_message: str | None = None) -> None:
97
+ """Clear execution status and reset parameters.
98
+
99
+ Args:
100
+ initial_message: Initial message to set in result_details. If None, clears result_details entirely.
101
+ """
102
+ if initial_message is None:
103
+ initial_message = ""
104
+ self.set_execution_result(was_successful=False, result_details=initial_message)
105
+
106
+ def append_to_result_details(self, additional_text: str, separator: str = "\n") -> None:
107
+ """Append text to the existing result_details.
108
+
109
+ Args:
110
+ additional_text: Text to append to the current result_details
111
+ separator: Separator to use between existing and new text (default: newline)
112
+ """
113
+ # Get current result_details value
114
+ current_details = self._node.get_parameter_value(self._result_details.name)
115
+
116
+ # Append the new text
117
+ if current_details:
118
+ updated_details = f"{current_details}{separator}{additional_text}"
119
+ else:
120
+ updated_details = additional_text
121
+
122
+ # Use consolidated update method
123
+ self._update_parameter_value(self._result_details, updated_details)
124
+
125
+ def _update_parameter_value(self, parameter: Parameter, value: Any) -> None:
126
+ """Update a parameter value with all necessary operations.
127
+
128
+ Args:
129
+ parameter: The parameter to update
130
+ value: The new value to set
131
+ """
132
+ # ALWAYS set parameter value and publish update
133
+ self._node.set_parameter_value(parameter.name, value)
134
+ self._node.publish_update_to_parameter(parameter.name, value)
135
+
136
+ # ONLY set output values if the parameter mode is OUTPUT
137
+ if ParameterMode.OUTPUT in parameter.get_mode():
138
+ self._node.parameter_output_values[parameter.name] = value