griptape-nodes 0.44.0__tar.gz → 0.45.0__tar.gz

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 (123) hide show
  1. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/PKG-INFO +2 -1
  2. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/pyproject.toml +2 -1
  3. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/__init__.py +5 -1
  4. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/app/api.py +2 -35
  5. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/app/app.py +70 -3
  6. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/app/watch.py +5 -2
  7. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/drivers/storage/base_storage_driver.py +37 -0
  8. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +2 -1
  9. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/exe_types/core_types.py +109 -9
  10. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/exe_types/node_types.py +19 -5
  11. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/node_library/workflow_registry.py +29 -0
  12. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/app_events.py +3 -2
  13. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/base_events.py +9 -0
  14. griptape_nodes-0.45.0/src/griptape_nodes/retained_mode/events/sync_events.py +60 -0
  15. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/workflow_events.py +231 -0
  16. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/griptape_nodes.py +8 -0
  17. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/library_manager.py +6 -18
  18. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/node_manager.py +2 -2
  19. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/operation_manager.py +7 -0
  20. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/settings.py +5 -0
  21. griptape_nodes-0.45.0/src/griptape_nodes/retained_mode/managers/sync_manager.py +498 -0
  22. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/workflow_manager.py +682 -28
  23. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/retained_mode.py +23 -0
  24. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/updater/__init__.py +4 -2
  25. griptape_nodes-0.45.0/src/griptape_nodes/utils/uv_utils.py +18 -0
  26. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/README.md +0 -0
  27. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/app/.python-version +0 -0
  28. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/app/__init__.py +0 -0
  29. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/bootstrap/__init__.py +0 -0
  30. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/bootstrap/workflow_executors/__init__.py +0 -0
  31. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +0 -0
  32. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +0 -0
  33. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +0 -0
  34. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/drivers/__init__.py +0 -0
  35. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/drivers/storage/__init__.py +0 -0
  36. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/drivers/storage/local_storage_driver.py +0 -0
  37. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/drivers/storage/storage_backend.py +0 -0
  38. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/exe_types/__init__.py +0 -0
  39. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/exe_types/connections.py +0 -0
  40. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/exe_types/flow.py +0 -0
  41. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/exe_types/type_validator.py +0 -0
  42. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/machines/__init__.py +0 -0
  43. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/machines/control_flow.py +0 -0
  44. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/machines/fsm.py +0 -0
  45. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/machines/node_resolution.py +0 -0
  46. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/mcp_server/__init__.py +0 -0
  47. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/mcp_server/server.py +0 -0
  48. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/mcp_server/ws_request_manager.py +0 -0
  49. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/node_library/__init__.py +0 -0
  50. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/node_library/advanced_node_library.py +0 -0
  51. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/node_library/library_registry.py +0 -0
  52. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/py.typed +0 -0
  53. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/__init__.py +0 -0
  54. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/__init__.py +0 -0
  55. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/agent_events.py +0 -0
  56. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/arbitrary_python_events.py +0 -0
  57. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/config_events.py +0 -0
  58. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/connection_events.py +0 -0
  59. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/context_events.py +0 -0
  60. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/execution_events.py +0 -0
  61. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/flow_events.py +0 -0
  62. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +0 -0
  63. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/library_events.py +0 -0
  64. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/logger_events.py +0 -0
  65. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/node_events.py +0 -0
  66. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/object_events.py +0 -0
  67. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/os_events.py +0 -0
  68. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/parameter_events.py +0 -0
  69. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/payload_registry.py +0 -0
  70. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/secrets_events.py +0 -0
  71. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/static_file_events.py +0 -0
  72. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/events/validation_events.py +0 -0
  73. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/__init__.py +0 -0
  74. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/agent_manager.py +0 -0
  75. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +0 -0
  76. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/config_manager.py +0 -0
  77. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/context_manager.py +0 -0
  78. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/engine_identity_manager.py +0 -0
  79. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/event_manager.py +0 -0
  80. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/flow_manager.py +0 -0
  81. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +0 -0
  82. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +0 -0
  83. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +0 -0
  84. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +0 -0
  85. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +0 -0
  86. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +0 -0
  87. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +0 -0
  88. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +0 -0
  89. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +0 -0
  90. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +0 -0
  91. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +0 -0
  92. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +0 -0
  93. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/object_manager.py +0 -0
  94. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/os_manager.py +0 -0
  95. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/secrets_manager.py +0 -0
  96. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/session_manager.py +0 -0
  97. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/static_files_manager.py +0 -0
  98. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/managers/version_compatibility_manager.py +0 -0
  99. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/utils/__init__.py +0 -0
  100. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/utils/engine_identity.py +0 -0
  101. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/retained_mode/utils/name_generator.py +0 -0
  102. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/traits/__init__.py +0 -0
  103. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/traits/add_param_button.py +0 -0
  104. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/traits/button.py +0 -0
  105. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/traits/clamp.py +0 -0
  106. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/traits/compare.py +0 -0
  107. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/traits/compare_images.py +0 -0
  108. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/traits/file_system_picker.py +0 -0
  109. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/traits/minmax.py +0 -0
  110. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/traits/options.py +0 -0
  111. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/traits/slider.py +0 -0
  112. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/traits/trait_registry.py +0 -0
  113. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/traits/traits.json +0 -0
  114. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/updater/__main__.py +0 -0
  115. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/utils/__init__.py +0 -0
  116. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/utils/dict_utils.py +0 -0
  117. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/utils/image_preview.py +0 -0
  118. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/utils/metaclasses.py +0 -0
  119. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/utils/version_utils.py +0 -0
  120. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/version_compatibility/__init__.py +0 -0
  121. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/version_compatibility/versions/__init__.py +0 -0
  122. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py +0 -0
  123. {griptape_nodes-0.44.0 → griptape_nodes-0.45.0}/src/griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: griptape-nodes
3
- Version: 0.44.0
3
+ Version: 0.45.0
4
4
  Summary: Add your description here
5
5
  Requires-Dist: griptape>=1.8.0
6
6
  Requires-Dist: pydantic>=2.10.6
@@ -19,6 +19,7 @@ Requires-Dist: imageio-ffmpeg>=0.6.0
19
19
  Requires-Dist: mcp[ws]>=1.10.1
20
20
  Requires-Dist: binaryornot>=0.4.4
21
21
  Requires-Dist: pillow>=11.3.0
22
+ Requires-Dist: watchfiles>=1.1.0
22
23
  Requires-Dist: austin-dist>=3.7.0 ; extra == 'profiling'
23
24
  Requires-Python: >=3.12.0, <3.13
24
25
  Provides-Extra: profiling
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "griptape-nodes"
3
- version = "0.44.0"
3
+ version = "0.45.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12.0, <3.13"
@@ -23,6 +23,7 @@ dependencies = [
23
23
  "mcp[ws]>=1.10.1",
24
24
  "binaryornot>=0.4.4",
25
25
  "pillow>=11.3.0",
26
+ "watchfiles>=1.1.0",
26
27
  ]
27
28
 
28
29
  [project.optional-dependencies]
@@ -31,6 +31,7 @@ with console.status("Loading Griptape Nodes...") as status:
31
31
  from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
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
+ from griptape_nodes.utils.uv_utils import find_uv_bin
34
35
  from griptape_nodes.utils.version_utils import get_complete_version_string, get_current_version, get_install_source
35
36
 
36
37
  CONFIG_DIR = xdg_config_home() / "griptape_nodes"
@@ -808,7 +809,10 @@ def _uninstall_self() -> None:
808
809
  # Remove the executable
809
810
  console.print("[bold]Removing the executable...[/bold]")
810
811
  console.print("[bold yellow]When done, press Enter to exit.[/bold yellow]")
811
- os_manager.replace_process(["uv", "tool", "uninstall", "griptape-nodes"])
812
+
813
+ # Remove the tool using UV
814
+ uv_path = find_uv_bin()
815
+ os_manager.replace_process([uv_path, "tool", "uninstall", "griptape-nodes"])
812
816
 
813
817
 
814
818
  def _parse_key_value_pairs(pairs: list[str] | None) -> dict[str, Any] | None:
@@ -13,8 +13,6 @@ from fastapi.middleware.cors import CORSMiddleware
13
13
  from fastapi.staticfiles import StaticFiles
14
14
  from rich.logging import RichHandler
15
15
 
16
- from griptape_nodes.retained_mode.events.base_events import EventRequest, deserialize_event
17
-
18
16
  if TYPE_CHECKING:
19
17
  from queue import Queue
20
18
 
@@ -159,6 +157,8 @@ async def _delete_static_file(file_path: str, static_directory: Annotated[Path,
159
157
 
160
158
  @app.post("/engines/request")
161
159
  async def _create_event(request: Request, queue: Annotated[Queue, Depends(get_event_queue)]) -> None:
160
+ from .app import _process_api_event
161
+
162
162
  body = await request.json()
163
163
  _process_api_event(body, queue)
164
164
 
@@ -193,36 +193,3 @@ def start_api(static_directory: Path, queue: Queue) -> None:
193
193
  uvicorn.run(
194
194
  app, host=STATIC_SERVER_HOST, port=STATIC_SERVER_PORT, log_level=STATIC_SERVER_LOG_LEVEL, log_config=None
195
195
  )
196
-
197
-
198
- def _process_api_event(event: dict, event_queue: Queue) -> None:
199
- """Process API events and send them to the event queue."""
200
- payload = event.get("payload", {})
201
-
202
- try:
203
- payload["request"]
204
- except KeyError:
205
- msg = "Error: 'request' was expected but not found."
206
- raise RuntimeError(msg) from None
207
-
208
- try:
209
- event_type = payload["event_type"]
210
- if event_type != "EventRequest":
211
- msg = "Error: 'event_type' was found on request, but did not match 'EventRequest' as expected."
212
- raise RuntimeError(msg) from None
213
- except KeyError:
214
- msg = "Error: 'event_type' not found in request."
215
- raise RuntimeError(msg) from None
216
-
217
- # Now attempt to convert it into an EventRequest.
218
- try:
219
- request_event = deserialize_event(json_data=payload)
220
- if not isinstance(request_event, EventRequest):
221
- msg = f"Deserialized event is not an EventRequest: {type(request_event)}"
222
- raise TypeError(msg) # noqa: TRY301
223
- except Exception as e:
224
- msg = f"Unable to convert request JSON into a valid EventRequest object. Error Message: '{e}'"
225
- raise RuntimeError(msg) from None
226
-
227
- # Add the event to the queue
228
- event_queue.put(request_event)
@@ -9,7 +9,7 @@ import sys
9
9
  import threading
10
10
  from pathlib import Path
11
11
  from queue import Queue
12
- from typing import Any
12
+ from typing import Any, cast
13
13
  from urllib.parse import urljoin
14
14
 
15
15
  from griptape.events import (
@@ -24,9 +24,9 @@ from websockets.asyncio.client import connect
24
24
  from websockets.exceptions import ConnectionClosed, WebSocketException
25
25
 
26
26
  from griptape_nodes.mcp_server.server import main as mcp_server
27
+ from griptape_nodes.retained_mode.events import app_events, execution_events
27
28
 
28
29
  # This import is necessary to register all events, even if not technically used
29
- from griptape_nodes.retained_mode.events import app_events, execution_events
30
30
  from griptape_nodes.retained_mode.events.base_events import (
31
31
  AppEvent,
32
32
  EventRequest,
@@ -36,11 +36,14 @@ from griptape_nodes.retained_mode.events.base_events import (
36
36
  ExecutionGriptapeNodeEvent,
37
37
  GriptapeNodeEvent,
38
38
  ProgressEvent,
39
+ RequestPayload,
40
+ SkipTheLineMixin,
41
+ deserialize_event,
39
42
  )
40
43
  from griptape_nodes.retained_mode.events.logger_events import LogHandlerEvent
41
44
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
42
45
 
43
- from .api import _process_api_event, start_api
46
+ from .api import start_api
44
47
 
45
48
  # This is a global event queue that will be used to pass events between threads
46
49
  event_queue = Queue()
@@ -395,3 +398,67 @@ def __schedule_async_task(coro: Any) -> None:
395
398
  asyncio.run_coroutine_threadsafe(coro, event_loop)
396
399
  else:
397
400
  logger.warning("Event loop not available for scheduling async task")
401
+
402
+
403
+ def _process_api_event(event: dict, event_queue: Queue) -> None:
404
+ """Process API events and send them to the event queue."""
405
+ payload = event.get("payload", {})
406
+
407
+ try:
408
+ payload["request"]
409
+ except KeyError:
410
+ msg = "Error: 'request' was expected but not found."
411
+ raise RuntimeError(msg) from None
412
+
413
+ try:
414
+ event_type = payload["event_type"]
415
+ if event_type != "EventRequest":
416
+ msg = "Error: 'event_type' was found on request, but did not match 'EventRequest' as expected."
417
+ raise RuntimeError(msg) from None
418
+ except KeyError:
419
+ msg = "Error: 'event_type' not found in request."
420
+ raise RuntimeError(msg) from None
421
+
422
+ # Now attempt to convert it into an EventRequest.
423
+ try:
424
+ request_event = deserialize_event(json_data=payload)
425
+ if not isinstance(request_event, EventRequest):
426
+ msg = f"Deserialized event is not an EventRequest: {type(request_event)}"
427
+ raise TypeError(msg) # noqa: TRY301
428
+ except Exception as e:
429
+ msg = f"Unable to convert request JSON into a valid EventRequest object. Error Message: '{e}'"
430
+ raise RuntimeError(msg) from None
431
+
432
+ # Check if the event implements SkipTheLineMixin for priority processing
433
+ if isinstance(request_event.request, SkipTheLineMixin):
434
+ # Handle the event immediately without queuing
435
+ # The request is guaranteed to be a RequestPayload since it passed earlier validation
436
+ result_payload = GriptapeNodes.handle_request(
437
+ cast("RequestPayload", request_event.request),
438
+ response_topic=request_event.response_topic,
439
+ request_id=request_event.request_id,
440
+ )
441
+
442
+ # Create the result event and emit response immediately
443
+ if result_payload.succeeded():
444
+ result_event = EventResultSuccess(
445
+ request=cast("RequestPayload", request_event.request),
446
+ request_id=request_event.request_id,
447
+ result=result_payload,
448
+ response_topic=request_event.response_topic,
449
+ )
450
+ dest_socket = "success_result"
451
+ else:
452
+ result_event = EventResultFailure(
453
+ request=cast("RequestPayload", request_event.request),
454
+ request_id=request_event.request_id,
455
+ result=result_payload,
456
+ response_topic=request_event.response_topic,
457
+ )
458
+ dest_socket = "failure_result"
459
+
460
+ # Emit the response immediately
461
+ __schedule_async_task(__emit_message(dest_socket, result_event.json(), topic=result_event.response_topic))
462
+ else:
463
+ # Add the event to the queue for normal processing
464
+ event_queue.put(request_event)
@@ -8,6 +8,8 @@ from typing import Any
8
8
  from watchdog.events import PatternMatchingEventHandler
9
9
  from watchdog.observers import Observer
10
10
 
11
+ from griptape_nodes.utils.uv_utils import find_uv_bin
12
+
11
13
 
12
14
  class ReloadHandler(PatternMatchingEventHandler):
13
15
  def __init__(
@@ -30,8 +32,9 @@ class ReloadHandler(PatternMatchingEventHandler):
30
32
  def start_process(self) -> None:
31
33
  if self.process:
32
34
  self.process.terminate()
33
- self.process = subprocess.Popen(
34
- ["uv", "run", "gtn"], # noqa: S607
35
+ uv_path = find_uv_bin()
36
+ self.process = subprocess.Popen( # noqa: S603
37
+ [uv_path, "run", "gtn"],
35
38
  stdout=sys.stdout,
36
39
  stderr=sys.stderr,
37
40
  )
@@ -60,6 +60,43 @@ class BaseStorageDriver(ABC):
60
60
  """
61
61
  ...
62
62
 
63
+ def upload_file(self, file_name: str, file_content: bytes) -> str:
64
+ """Upload a file to storage.
65
+
66
+ Args:
67
+ file_name: The name of the file to upload.
68
+ file_content: The file content as bytes.
69
+
70
+ Returns:
71
+ The URL where the file can be accessed.
72
+
73
+ Raises:
74
+ RuntimeError: If file upload fails.
75
+ """
76
+ try:
77
+ # Get signed upload URL
78
+ upload_response = self.create_signed_upload_url(file_name)
79
+
80
+ # Upload the file using the signed URL
81
+ response = httpx.request(
82
+ upload_response["method"],
83
+ upload_response["url"],
84
+ content=file_content,
85
+ headers=upload_response["headers"],
86
+ )
87
+ response.raise_for_status()
88
+
89
+ # Return the download URL
90
+ return self.create_signed_download_url(file_name)
91
+ except httpx.HTTPStatusError as e:
92
+ msg = f"Failed to upload file {file_name}: {e}"
93
+ logger.error(msg)
94
+ raise RuntimeError(msg) from e
95
+ except Exception as e:
96
+ msg = f"Unexpected error uploading file {file_name}: {e}"
97
+ logger.error(msg)
98
+ raise RuntimeError(msg) from e
99
+
63
100
  def download_file(self, file_name: str) -> bytes:
64
101
  """Download a file from the bucket.
65
102
 
@@ -76,7 +76,8 @@ class GriptapeCloudStorageDriver(BaseStorageDriver):
76
76
  return {"url": response_data["url"], "headers": response_data.get("headers", {}), "method": "PUT"}
77
77
 
78
78
  def create_signed_download_url(self, file_name: str) -> str:
79
- url = urljoin(self.base_url, f"/api/buckets/{self.bucket_id}/asset-urls/{file_name}")
79
+ full_file_path = self._get_full_file_path(file_name)
80
+ url = urljoin(self.base_url, f"/api/buckets/{self.bucket_id}/asset-urls/{full_file_path}")
80
81
  try:
81
82
  response = httpx.post(url, json={"method": "GET"}, headers=self.headers)
82
83
  response.raise_for_status()
@@ -300,11 +300,28 @@ class BaseNodeElement:
300
300
  self._node_context._emit_parameter_lifecycle_event(child)
301
301
 
302
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
+ """
303
313
  ui_elements: list[BaseNodeElement] = [self]
304
314
  for ui_element in ui_elements:
305
315
  if child in ui_element._children:
306
- child._parent = None
307
- 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)
308
325
  break
309
326
  ui_elements.extend(ui_element._children)
310
327
  if self._node_context is not None and isinstance(child, BaseNodeElement):
@@ -371,8 +388,23 @@ class BaseNodeElement:
371
388
  return event_data
372
389
 
373
390
 
374
- @dataclass(kw_only=True)
375
- 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):
376
408
  """Represents a UI message element, such as a warning or informational text."""
377
409
 
378
410
  # Define default titles as a class-level constant
@@ -518,11 +550,21 @@ class ParameterMessage(BaseNodeElement):
518
550
  return event_data
519
551
 
520
552
 
521
- @dataclass(kw_only=True)
522
- class ParameterGroup(BaseNodeElement):
553
+ class ParameterGroup(BaseNodeElement, UIOptionsMixin):
523
554
  """UI element for a group of parameters."""
524
555
 
525
- 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
526
568
 
527
569
  def to_dict(self) -> dict[str, Any]:
528
570
  """Returns a nested dictionary representation of this node and its children.
@@ -630,7 +672,7 @@ class ParameterBase(BaseNodeElement, ABC):
630
672
  pass
631
673
 
632
674
 
633
- class Parameter(BaseNodeElement):
675
+ class Parameter(BaseNodeElement, UIOptionsMixin):
634
676
  # This is the list of types that the Parameter can accept, either externally or when internally treated as a property.
635
677
  # Today, we can accept multiple types for input, but only a single output type.
636
678
  tooltip: str | list[dict] # Default tooltip, can be string or list of dicts
@@ -641,7 +683,11 @@ class Parameter(BaseNodeElement):
641
683
  tooltip_as_input: str | list[dict] | None = None
642
684
  tooltip_as_property: str | list[dict] | None = None
643
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.
644
689
  settable: bool = True
690
+
645
691
  user_defined: bool = False
646
692
  _allowed_modes: set = field(
647
693
  default_factory=lambda: {
@@ -837,7 +883,10 @@ class Parameter(BaseNodeElement):
837
883
  ui_options = ui_options | trait.ui_options_for_trait()
838
884
  ui_options = ui_options | self._ui_options
839
885
  if self._parent is not None and isinstance(self._parent, ParameterGroup):
840
- 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
841
890
  return ui_options
842
891
 
843
892
  @ui_options.setter
@@ -1198,6 +1247,23 @@ class ParameterContainer(Parameter, ABC):
1198
1247
  element_type=element_type,
1199
1248
  )
1200
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
+
1201
1267
  @abstractmethod
1202
1268
  def add_child_parameter(self) -> Parameter:
1203
1269
  pass
@@ -1329,6 +1395,40 @@ class ParameterList(ParameterContainer):
1329
1395
 
1330
1396
  return param
1331
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
+
1332
1432
 
1333
1433
  class ParameterKeyValuePair(Parameter):
1334
1434
  def __init__( # noqa: PLR0913
@@ -368,8 +368,10 @@ class BaseNode(ABC):
368
368
  """
369
369
  parameter = self.get_parameter_by_name(param)
370
370
  if parameter is not None:
371
- trait = parameter.find_element_by_id("Options")
372
- 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
373
375
  trait.choices = choices
374
376
 
375
377
  if default in choices:
@@ -378,6 +380,13 @@ class BaseNode(ABC):
378
380
  else:
379
381
  msg = f"Default model '{default}' is not in the provided choices."
380
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)
381
390
  else:
382
391
  msg = f"Parameter '{param}' not found for updating model choices."
383
392
  raise ValueError(msg)
@@ -393,9 +402,14 @@ class BaseNode(ABC):
393
402
  """
394
403
  parameter = self.get_parameter_by_name(param)
395
404
  if parameter is not None:
396
- trait = parameter.find_element_by_id("Options")
397
- 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
398
409
  parameter.remove_trait(trait)
410
+ else:
411
+ msg = f"No Options trait found for parameter '{param}'."
412
+ raise ValueError(msg)
399
413
  else:
400
414
  msg = f"Parameter '{param}' not found for removing options trait."
401
415
  raise ValueError(msg)
@@ -576,7 +590,7 @@ class BaseNode(ABC):
576
590
  param = self.get_parameter_by_name(param_name)
577
591
  if param and isinstance(param, ParameterContainer):
578
592
  value = handle_container_parameter(self, param)
579
- if value:
593
+ if value is not None:
580
594
  return value
581
595
  if param_name in self.parameter_values:
582
596
  return self.parameter_values[param_name]
@@ -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
@@ -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
@@ -148,7 +149,7 @@ class AppEndSessionResultFailure(ResultPayloadFailure):
148
149
 
149
150
  @dataclass
150
151
  @PayloadRegistry.register
151
- class SessionHeartbeatRequest(RequestPayload):
152
+ class SessionHeartbeatRequest(RequestPayload, SkipTheLineMixin):
152
153
  """Request clients can use ensure the engine session is still active."""
153
154
 
154
155
 
@@ -166,7 +167,7 @@ class SessionHeartbeatResultFailure(ResultPayloadFailure):
166
167
 
167
168
  @dataclass
168
169
  @PayloadRegistry.register
169
- class EngineHeartbeatRequest(RequestPayload):
170
+ class EngineHeartbeatRequest(RequestPayload, SkipTheLineMixin):
170
171
  """Request clients can use to discover active engines and their status.
171
172
 
172
173
  Attributes:
@@ -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):