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
@@ -135,6 +135,37 @@ class GriptapeCloudStorageDriver(BaseStorageDriver):
135
135
  logger.info("Created new Griptape Cloud bucket '%s' with ID: %s", bucket_name, bucket_id)
136
136
  return bucket_id
137
137
 
138
+ def list_files(self) -> list[str]:
139
+ """List all files in storage.
140
+
141
+ Returns:
142
+ A list of file names in storage.
143
+
144
+ Raises:
145
+ RuntimeError: If file listing fails.
146
+ """
147
+ url = urljoin(self.base_url, f"/api/buckets/{self.bucket_id}/assets")
148
+ try:
149
+ response = httpx.get(url, headers=self.headers, params={"prefix": self.static_files_directory or ""})
150
+ response.raise_for_status()
151
+ except httpx.HTTPStatusError as e:
152
+ msg = f"Failed to list files in bucket {self.bucket_id}: {e}"
153
+ logger.error(msg)
154
+ raise RuntimeError(msg) from e
155
+
156
+ response_data = response.json()
157
+ assets = response_data.get("assets", [])
158
+
159
+ file_names = []
160
+ for asset in assets:
161
+ name = asset.get("name", "")
162
+ # Remove the static files directory prefix if it exists
163
+ if self.static_files_directory and name.startswith(f"{self.static_files_directory}/"):
164
+ name = name[len(f"{self.static_files_directory}/") :]
165
+ file_names.append(name)
166
+
167
+ return file_names
168
+
138
169
  @staticmethod
139
170
  def list_buckets(*, base_url: str, api_key: str) -> list[dict]:
140
171
  """List all buckets in Griptape Cloud.
@@ -158,3 +189,20 @@ class GriptapeCloudStorageDriver(BaseStorageDriver):
158
189
  raise RuntimeError(msg) from e
159
190
 
160
191
  return response.json().get("buckets", [])
192
+
193
+ def delete_file(self, file_name: str) -> None:
194
+ """Delete a file from the bucket.
195
+
196
+ Args:
197
+ file_name: The name of the file to delete.
198
+ """
199
+ full_file_path = self._get_full_file_path(file_name)
200
+ url = urljoin(self.base_url, f"/api/buckets/{self.bucket_id}/assets/{full_file_path}")
201
+
202
+ try:
203
+ response = httpx.delete(url, headers=self.headers)
204
+ response.raise_for_status()
205
+ except httpx.HTTPStatusError as e:
206
+ msg = f"Failed to delete file {file_name}: {e}"
207
+ logger.error(msg)
208
+ raise RuntimeError(msg) from e
@@ -53,3 +53,40 @@ class LocalStorageDriver(BaseStorageDriver):
53
53
  # Add a cache-busting query parameter to the URL so that the browser always reloads the file
54
54
  cache_busted_url = f"{url}?t={int(time.time())}"
55
55
  return cache_busted_url
56
+
57
+ def delete_file(self, file_name: str) -> None:
58
+ """Delete a file from local storage.
59
+
60
+ Args:
61
+ file_name: The name of the file to delete.
62
+ """
63
+ # Use the static server's delete endpoint
64
+ delete_url = urljoin(self.base_url, f"/static-files/{file_name}")
65
+
66
+ try:
67
+ response = httpx.delete(delete_url)
68
+ response.raise_for_status()
69
+ except httpx.HTTPStatusError as e:
70
+ msg = f"Failed to delete file {file_name}: {e}"
71
+ logger.error(msg)
72
+ raise RuntimeError(msg) from e
73
+
74
+ def list_files(self) -> list[str]:
75
+ """List all files in local storage.
76
+
77
+ Returns:
78
+ A list of file names in storage.
79
+ """
80
+ # Use the static server's list endpoint
81
+ list_url = urljoin(self.base_url, "/static-uploads/")
82
+
83
+ try:
84
+ response = httpx.get(list_url)
85
+ response.raise_for_status()
86
+ except httpx.HTTPStatusError as e:
87
+ msg = f"Failed to list files: {e}"
88
+ logger.error(msg)
89
+ raise RuntimeError(msg) from e
90
+
91
+ response_data = response.json()
92
+ return response_data.get("files", [])
File without changes
File without changes
File without changes
@@ -244,6 +244,11 @@ class BaseNodeElement:
244
244
  "name": self.name,
245
245
  "node_name": self._node_context.name,
246
246
  }
247
+ # If ui_options changed, send the complete ui_options from to_dict()
248
+ complete_dict = self.to_dict()
249
+ if "ui_options" in complete_dict:
250
+ self._changes["ui_options"] = complete_dict["ui_options"]
251
+
247
252
  event_data.update(self._changes)
248
253
 
249
254
  # Publish the event
@@ -295,11 +300,28 @@ class BaseNodeElement:
295
300
  self._node_context._emit_parameter_lifecycle_event(child)
296
301
 
297
302
  def remove_child(self, child: BaseNodeElement | str) -> None:
303
+ """Remove a child element from the hierarchy.
304
+
305
+ This method recursively searches through the element hierarchy to find and remove
306
+ the specified child. When the child is found in a descendant container (e.g., a
307
+ ParameterList), it delegates to that container's remove_child() method to ensure
308
+ proper cleanup and event handling (like marking parent nodes as unresolved).
309
+
310
+ Args:
311
+ child: The child element to remove, either as an object or by name string
312
+ """
298
313
  ui_elements: list[BaseNodeElement] = [self]
299
314
  for ui_element in ui_elements:
300
315
  if child in ui_element._children:
301
- child._parent = None
302
- ui_element._children.remove(child)
316
+ # Delegate to the actual parent container's remove_child method.
317
+ # This ensures specialized containers (like ParameterList) can perform
318
+ # their specific cleanup logic (e.g., marking parent nodes as unresolved).
319
+ if ui_element is not self:
320
+ ui_element.remove_child(child)
321
+ else:
322
+ # We are the direct parent, so handle removal directly
323
+ child._parent = None
324
+ ui_element._children.remove(child)
303
325
  break
304
326
  ui_elements.extend(ui_element._children)
305
327
  if self._node_context is not None and isinstance(child, BaseNodeElement):
@@ -366,8 +388,23 @@ class BaseNodeElement:
366
388
  return event_data
367
389
 
368
390
 
369
- @dataclass(kw_only=True)
370
- class ParameterMessage(BaseNodeElement):
391
+ class UIOptionsMixin:
392
+ """Mixin providing UI options update functionality for classes with ui_options."""
393
+
394
+ def update_ui_options_key(self, key: str, value: Any) -> None:
395
+ """Update a single UI option key."""
396
+ ui_options = self.ui_options
397
+ ui_options[key] = value
398
+ self.ui_options = ui_options
399
+
400
+ def update_ui_options(self, updates: dict[str, Any]) -> None:
401
+ """Update multiple UI options at once."""
402
+ ui_options = self.ui_options
403
+ ui_options.update(updates)
404
+ self.ui_options = ui_options
405
+
406
+
407
+ class ParameterMessage(BaseNodeElement, UIOptionsMixin):
371
408
  """Represents a UI message element, such as a warning or informational text."""
372
409
 
373
410
  # Define default titles as a class-level constant
@@ -384,13 +421,97 @@ class ParameterMessage(BaseNodeElement):
384
421
  type VariantType = Literal["info", "warning", "error", "success", "tip", "none"]
385
422
 
386
423
  element_type: str = field(default_factory=lambda: ParameterMessage.__name__)
387
- variant: VariantType
388
- title: str | None = None
389
- value: str
390
- button_link: str | None = None
391
- button_text: str | None = None
392
- full_width: bool = False
393
- ui_options: dict = field(default_factory=dict)
424
+ _variant: VariantType = field(init=False)
425
+ _title: str | None = field(default=None, init=False)
426
+ _value: str = field(init=False)
427
+ _button_link: str | None = field(default=None, init=False)
428
+ _button_text: str | None = field(default=None, init=False)
429
+ _full_width: bool = field(default=False, init=False)
430
+ _ui_options: dict = field(default_factory=dict, init=False)
431
+
432
+ def __init__( # noqa: PLR0913
433
+ self,
434
+ variant: VariantType,
435
+ value: str,
436
+ *,
437
+ title: str | None = None,
438
+ button_link: str | None = None,
439
+ button_text: str | None = None,
440
+ full_width: bool = False,
441
+ ui_options: dict | None = None,
442
+ **kwargs,
443
+ ):
444
+ super().__init__(element_type=ParameterMessage.__name__, **kwargs)
445
+ self._variant = variant
446
+ self._title = title
447
+ self._value = value
448
+ self._button_link = button_link
449
+ self._button_text = button_text
450
+ self._full_width = full_width
451
+ self._ui_options = ui_options or {}
452
+
453
+ @property
454
+ def variant(self) -> VariantType:
455
+ return self._variant
456
+
457
+ @variant.setter
458
+ @BaseNodeElement.emits_update_on_write
459
+ def variant(self, value: VariantType) -> None:
460
+ self._variant = value
461
+
462
+ @property
463
+ def title(self) -> str | None:
464
+ return self._title
465
+
466
+ @title.setter
467
+ @BaseNodeElement.emits_update_on_write
468
+ def title(self, value: str | None) -> None:
469
+ self._title = value
470
+
471
+ @property
472
+ def value(self) -> str:
473
+ return self._value
474
+
475
+ @value.setter
476
+ @BaseNodeElement.emits_update_on_write
477
+ def value(self, value: str) -> None:
478
+ self._value = value
479
+
480
+ @property
481
+ def button_link(self) -> str | None:
482
+ return self._button_link
483
+
484
+ @button_link.setter
485
+ @BaseNodeElement.emits_update_on_write
486
+ def button_link(self, value: str | None) -> None:
487
+ self._button_link = value
488
+
489
+ @property
490
+ def button_text(self) -> str | None:
491
+ return self._button_text
492
+
493
+ @button_text.setter
494
+ @BaseNodeElement.emits_update_on_write
495
+ def button_text(self, value: str | None) -> None:
496
+ self._button_text = value
497
+
498
+ @property
499
+ def full_width(self) -> bool:
500
+ return self._full_width
501
+
502
+ @full_width.setter
503
+ @BaseNodeElement.emits_update_on_write
504
+ def full_width(self, value: bool) -> None:
505
+ self._full_width = value
506
+
507
+ @property
508
+ def ui_options(self) -> dict:
509
+ return self._ui_options
510
+
511
+ @ui_options.setter
512
+ @BaseNodeElement.emits_update_on_write
513
+ def ui_options(self, value: dict) -> None:
514
+ self._ui_options = value
394
515
 
395
516
  def to_dict(self) -> dict[str, Any]:
396
517
  data = super().to_dict()
@@ -429,11 +550,21 @@ class ParameterMessage(BaseNodeElement):
429
550
  return event_data
430
551
 
431
552
 
432
- @dataclass(kw_only=True)
433
- class ParameterGroup(BaseNodeElement):
553
+ class ParameterGroup(BaseNodeElement, UIOptionsMixin):
434
554
  """UI element for a group of parameters."""
435
555
 
436
- ui_options: dict = field(default_factory=dict)
556
+ def __init__(self, name: str, ui_options: dict | None = None, **kwargs):
557
+ super().__init__(name=name, **kwargs)
558
+ self._ui_options = ui_options or {}
559
+
560
+ @property
561
+ def ui_options(self) -> dict:
562
+ return self._ui_options
563
+
564
+ @ui_options.setter
565
+ @BaseNodeElement.emits_update_on_write
566
+ def ui_options(self, value: dict) -> None:
567
+ self._ui_options = value
437
568
 
438
569
  def to_dict(self) -> dict[str, Any]:
439
570
  """Returns a nested dictionary representation of this node and its children.
@@ -541,7 +672,7 @@ class ParameterBase(BaseNodeElement, ABC):
541
672
  pass
542
673
 
543
674
 
544
- class Parameter(BaseNodeElement):
675
+ class Parameter(BaseNodeElement, UIOptionsMixin):
545
676
  # This is the list of types that the Parameter can accept, either externally or when internally treated as a property.
546
677
  # Today, we can accept multiple types for input, but only a single output type.
547
678
  tooltip: str | list[dict] # Default tooltip, can be string or list of dicts
@@ -552,7 +683,11 @@ class Parameter(BaseNodeElement):
552
683
  tooltip_as_input: str | list[dict] | None = None
553
684
  tooltip_as_property: str | list[dict] | None = None
554
685
  tooltip_as_output: str | list[dict] | None = None
686
+
687
+ # "settable" here means whether it can be assigned to during regular business operation.
688
+ # During save/load, this value IS still serialized to save its proper state.
555
689
  settable: bool = True
690
+
556
691
  user_defined: bool = False
557
692
  _allowed_modes: set = field(
558
693
  default_factory=lambda: {
@@ -594,7 +729,7 @@ class Parameter(BaseNodeElement):
594
729
  if not element_id:
595
730
  element_id = str(uuid.uuid4().hex)
596
731
  if not element_type:
597
- element_type = BaseNodeElement.__name__
732
+ element_type = self.__class__.__name__
598
733
  super().__init__(element_id=element_id, element_type=element_type)
599
734
  self.name = name
600
735
  self.tooltip = tooltip
@@ -748,7 +883,10 @@ class Parameter(BaseNodeElement):
748
883
  ui_options = ui_options | trait.ui_options_for_trait()
749
884
  ui_options = ui_options | self._ui_options
750
885
  if self._parent is not None and isinstance(self._parent, ParameterGroup):
751
- ui_options = ui_options | self._parent.ui_options
886
+ # Access the field value directly for ParameterGroup
887
+ parent_ui_options = getattr(self._parent, "ui_options", {})
888
+ if isinstance(parent_ui_options, dict):
889
+ ui_options = ui_options | parent_ui_options
752
890
  return ui_options
753
891
 
754
892
  @ui_options.setter
@@ -951,6 +1089,7 @@ class ControlParameter(Parameter, ABC):
951
1089
  traits: set[Trait.__class__ | Trait] | None = None,
952
1090
  converters: list[Callable[[Any], Any]] | None = None,
953
1091
  validators: list[Callable[[Parameter, Any], None]] | None = None,
1092
+ ui_options: dict | None = None,
954
1093
  *,
955
1094
  user_defined: bool = False,
956
1095
  ):
@@ -970,6 +1109,7 @@ class ControlParameter(Parameter, ABC):
970
1109
  traits=traits,
971
1110
  converters=converters,
972
1111
  validators=validators,
1112
+ ui_options=ui_options,
973
1113
  user_defined=user_defined,
974
1114
  element_type=self.__class__.__name__,
975
1115
  )
@@ -980,6 +1120,7 @@ class ControlParameterInput(ControlParameter):
980
1120
  self,
981
1121
  tooltip: str | list[dict] = "Connection from previous node in the execution chain",
982
1122
  name: str = "exec_in",
1123
+ display_name: str | None = "Flow In",
983
1124
  tooltip_as_input: str | list[dict] | None = None,
984
1125
  tooltip_as_property: str | list[dict] | None = None,
985
1126
  tooltip_as_output: str | list[dict] | None = None,
@@ -992,6 +1133,11 @@ class ControlParameterInput(ControlParameter):
992
1133
  allowed_modes = {ParameterMode.INPUT}
993
1134
  input_types = [ParameterTypeBuiltin.CONTROL_TYPE.value]
994
1135
 
1136
+ if display_name is None:
1137
+ ui_options = None
1138
+ else:
1139
+ ui_options = {"display_name": display_name}
1140
+
995
1141
  # Call parent with a few explicit tweaks.
996
1142
  super().__init__(
997
1143
  name=name,
@@ -1005,6 +1151,7 @@ class ControlParameterInput(ControlParameter):
1005
1151
  traits=traits,
1006
1152
  converters=converters,
1007
1153
  validators=validators,
1154
+ ui_options=ui_options,
1008
1155
  user_defined=user_defined,
1009
1156
  )
1010
1157
 
@@ -1014,6 +1161,7 @@ class ControlParameterOutput(ControlParameter):
1014
1161
  self,
1015
1162
  tooltip: str | list[dict] = "Connection to the next node in the execution chain",
1016
1163
  name: str = "exec_out",
1164
+ display_name: str | None = "Flow Out",
1017
1165
  tooltip_as_input: str | list[dict] | None = None,
1018
1166
  tooltip_as_property: str | list[dict] | None = None,
1019
1167
  tooltip_as_output: str | list[dict] | None = None,
@@ -1026,6 +1174,11 @@ class ControlParameterOutput(ControlParameter):
1026
1174
  allowed_modes = {ParameterMode.OUTPUT}
1027
1175
  output_type = ParameterTypeBuiltin.CONTROL_TYPE.value
1028
1176
 
1177
+ if display_name is None:
1178
+ ui_options = None
1179
+ else:
1180
+ ui_options = {"display_name": display_name}
1181
+
1029
1182
  # Call parent with a few explicit tweaks.
1030
1183
  super().__init__(
1031
1184
  name=name,
@@ -1039,6 +1192,7 @@ class ControlParameterOutput(ControlParameter):
1039
1192
  traits=traits,
1040
1193
  converters=converters,
1041
1194
  validators=validators,
1195
+ ui_options=ui_options,
1042
1196
  user_defined=user_defined,
1043
1197
  )
1044
1198
 
@@ -1093,6 +1247,23 @@ class ParameterContainer(Parameter, ABC):
1093
1247
  element_type=element_type,
1094
1248
  )
1095
1249
 
1250
+ def __bool__(self) -> bool:
1251
+ """Parameter containers are always truthy, even when empty.
1252
+
1253
+ This overrides Python's default truthiness behavior for containers with __len__().
1254
+ By default, Python makes objects with __len__() falsy when len() == 0, which
1255
+ caused bugs where empty ParameterList/ParameterDictionary objects would fail
1256
+ 'if param' checks and fall back to stale cached values instead of computing
1257
+ fresh empty results.
1258
+
1259
+ Unlike standard Python containers, ParameterContainer objects represent
1260
+ parameter structure/definitions rather than just data, so they remain
1261
+ meaningful even when empty.
1262
+
1263
+ See: https://github.com/griptape-ai/griptape-nodes/issues/1799
1264
+ """
1265
+ return True
1266
+
1096
1267
  @abstractmethod
1097
1268
  def add_child_parameter(self) -> Parameter:
1098
1269
  pass
@@ -1224,6 +1395,40 @@ class ParameterList(ParameterContainer):
1224
1395
 
1225
1396
  return param
1226
1397
 
1398
+ def add_child(self, child: BaseNodeElement) -> None:
1399
+ """Override to mark parent node as unresolved when children are added.
1400
+
1401
+ When a ParameterList gains a child parameter, the parent node needs to be
1402
+ marked as unresolved to trigger re-evaluation of the node's state and outputs.
1403
+ """
1404
+ super().add_child(child)
1405
+
1406
+ # Mark the parent node as unresolved since the parameter structure changed
1407
+ if self._node_context is not None:
1408
+ # Import at runtime to avoid circular import
1409
+ from griptape_nodes.exe_types.node_types import NodeResolutionState
1410
+
1411
+ self._node_context.make_node_unresolved(
1412
+ current_states_to_trigger_change_event={NodeResolutionState.RESOLVED, NodeResolutionState.RESOLVING}
1413
+ )
1414
+
1415
+ def remove_child(self, child: BaseNodeElement | str) -> None:
1416
+ """Override to mark parent node as unresolved when children are removed.
1417
+
1418
+ When a ParameterList loses a child parameter, the parent node needs to be
1419
+ marked as unresolved to trigger re-evaluation of the node's state and outputs.
1420
+ """
1421
+ super().remove_child(child)
1422
+
1423
+ # Mark the parent node as unresolved since the parameter structure changed
1424
+ if self._node_context is not None:
1425
+ # Import at runtime to avoid circular import
1426
+ from griptape_nodes.exe_types.node_types import NodeResolutionState
1427
+
1428
+ self._node_context.make_node_unresolved(
1429
+ current_states_to_trigger_change_event={NodeResolutionState.RESOLVED, NodeResolutionState.RESOLVING}
1430
+ )
1431
+
1227
1432
 
1228
1433
  class ParameterKeyValuePair(Parameter):
1229
1434
  def __init__( # noqa: PLR0913
File without changes
@@ -68,6 +68,7 @@ class BaseNode(ABC):
68
68
  _entry_control_parameter: Parameter | None = (
69
69
  None # The control input parameter used to enter this node during execution
70
70
  )
71
+ lock: bool = False # When lock is true, the node is locked and can't be modified. When lock is false, the node is unlocked and can be modified.
71
72
 
72
73
  @property
73
74
  def parameters(self) -> list[Parameter]:
@@ -367,8 +368,10 @@ class BaseNode(ABC):
367
368
  """
368
369
  parameter = self.get_parameter_by_name(param)
369
370
  if parameter is not None:
370
- trait = parameter.find_element_by_id("Options")
371
- if trait and isinstance(trait, Options):
371
+ # Find the Options trait by type since element_id is a UUID
372
+ traits = parameter.find_elements_by_type(Options)
373
+ if traits:
374
+ trait = traits[0] # Take the first Options trait
372
375
  trait.choices = choices
373
376
 
374
377
  if default in choices:
@@ -377,6 +380,13 @@ class BaseNode(ABC):
377
380
  else:
378
381
  msg = f"Default model '{default}' is not in the provided choices."
379
382
  raise ValueError(msg)
383
+
384
+ # Update the manually set UI options to include the new simple_dropdown
385
+ if hasattr(parameter, "_ui_options") and parameter._ui_options:
386
+ parameter._ui_options["simple_dropdown"] = choices
387
+ else:
388
+ msg = f"No Options trait found for parameter '{param}'."
389
+ raise ValueError(msg)
380
390
  else:
381
391
  msg = f"Parameter '{param}' not found for updating model choices."
382
392
  raise ValueError(msg)
@@ -392,9 +402,14 @@ class BaseNode(ABC):
392
402
  """
393
403
  parameter = self.get_parameter_by_name(param)
394
404
  if parameter is not None:
395
- trait = parameter.find_element_by_id("Options")
396
- if trait and isinstance(trait, Options):
405
+ # Find the Options trait by type since element_id is a UUID
406
+ traits = parameter.find_elements_by_type(Options)
407
+ if traits:
408
+ trait = traits[0] # Take the first Options trait
397
409
  parameter.remove_trait(trait)
410
+ else:
411
+ msg = f"No Options trait found for parameter '{param}'."
412
+ raise ValueError(msg)
398
413
  else:
399
414
  msg = f"Parameter '{param}' not found for removing options trait."
400
415
  raise ValueError(msg)
@@ -575,7 +590,7 @@ class BaseNode(ABC):
575
590
  param = self.get_parameter_by_name(param_name)
576
591
  if param and isinstance(param, ParameterContainer):
577
592
  value = handle_container_parameter(self, param)
578
- if value:
593
+ if value is not None:
579
594
  return value
580
595
  if param_name in self.parameter_values:
581
596
  return self.parameter_values[param_name]
File without changes
File without changes
@@ -88,11 +88,12 @@ class ResolveNodeState(State):
88
88
  return CompleteState
89
89
 
90
90
  # Mark the node unresolved, and broadcast an event to the GUI.
91
- context.current_node.make_node_unresolved(
92
- current_states_to_trigger_change_event=set(
93
- {NodeResolutionState.UNRESOLVED, NodeResolutionState.RESOLVED, NodeResolutionState.RESOLVING}
91
+ if not context.current_node.lock:
92
+ context.current_node.make_node_unresolved(
93
+ current_states_to_trigger_change_event=set(
94
+ {NodeResolutionState.UNRESOLVED, NodeResolutionState.RESOLVED, NodeResolutionState.RESOLVING}
95
+ )
94
96
  )
95
- )
96
97
  # Now broadcast that we have a current control node.
97
98
  EventBus.publish_event(
98
99
  ExecutionGriptapeNodeEvent(
File without changes