griptape-nodes 0.70.0__py3-none-any.whl → 0.71.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 (26) hide show
  1. griptape_nodes/api_client/client.py +8 -5
  2. griptape_nodes/bootstrap/utils/python_subprocess_executor.py +48 -9
  3. griptape_nodes/bootstrap/utils/subprocess_websocket_base.py +88 -0
  4. griptape_nodes/bootstrap/utils/subprocess_websocket_listener.py +126 -0
  5. griptape_nodes/bootstrap/utils/subprocess_websocket_sender.py +121 -0
  6. griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +17 -170
  7. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +13 -117
  8. griptape_nodes/bootstrap/workflow_publishers/local_session_workflow_publisher.py +206 -0
  9. griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +22 -3
  10. griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +45 -25
  11. griptape_nodes/common/node_executor.py +60 -13
  12. griptape_nodes/exe_types/base_iterative_nodes.py +1 -1
  13. griptape_nodes/exe_types/node_groups/subflow_node_group.py +18 -0
  14. griptape_nodes/exe_types/param_components/log_parameter.py +1 -2
  15. griptape_nodes/exe_types/param_components/subflow_execution_component.py +329 -0
  16. griptape_nodes/exe_types/param_types/parameter_audio.py +17 -2
  17. griptape_nodes/exe_types/param_types/parameter_image.py +14 -1
  18. griptape_nodes/exe_types/param_types/parameter_three_d.py +14 -1
  19. griptape_nodes/exe_types/param_types/parameter_video.py +17 -2
  20. griptape_nodes/retained_mode/managers/os_manager.py +1 -1
  21. griptape_nodes/utils/artifact_normalization.py +245 -0
  22. griptape_nodes/utils/image_preview.py +27 -0
  23. {griptape_nodes-0.70.0.dist-info → griptape_nodes-0.71.0.dist-info}/METADATA +1 -1
  24. {griptape_nodes-0.70.0.dist-info → griptape_nodes-0.71.0.dist-info}/RECORD +26 -20
  25. {griptape_nodes-0.70.0.dist-info → griptape_nodes-0.71.0.dist-info}/WHEEL +0 -0
  26. {griptape_nodes-0.70.0.dist-info → griptape_nodes-0.71.0.dist-info}/entry_points.txt +0 -0
@@ -8,9 +8,11 @@ from typing import TYPE_CHECKING, Any, Self
8
8
  import anyio
9
9
 
10
10
  from griptape_nodes.bootstrap.utils.python_subprocess_executor import PythonSubprocessExecutor
11
+ from griptape_nodes.bootstrap.utils.subprocess_websocket_listener import SubprocessWebSocketListenerMixin
11
12
  from griptape_nodes.bootstrap.workflow_publishers.local_workflow_publisher import LocalWorkflowPublisher
12
13
 
13
14
  if TYPE_CHECKING:
15
+ from collections.abc import Callable
14
16
  from types import TracebackType
15
17
 
16
18
  logger = logging.getLogger(__name__)
@@ -20,11 +22,18 @@ class SubprocessWorkflowPublisherError(Exception):
20
22
  """Exception raised during subprocess workflow publishing."""
21
23
 
22
24
 
23
- class SubprocessWorkflowPublisher(LocalWorkflowPublisher, PythonSubprocessExecutor):
24
- def __init__(self) -> None:
25
+ class SubprocessWorkflowPublisher(LocalWorkflowPublisher, PythonSubprocessExecutor, SubprocessWebSocketListenerMixin):
26
+ def __init__(
27
+ self,
28
+ on_event: Callable[[dict], None] | None = None,
29
+ session_id: str | None = None,
30
+ ) -> None:
25
31
  PythonSubprocessExecutor.__init__(self)
32
+ self._init_websocket_listener(session_id=session_id, on_event=on_event)
26
33
 
27
34
  async def __aenter__(self) -> Self:
35
+ """Async context manager entry: start WebSocket listener."""
36
+ await self._start_websocket_listener()
28
37
  return self
29
38
 
30
39
  async def __aexit__(
@@ -33,7 +42,8 @@ class SubprocessWorkflowPublisher(LocalWorkflowPublisher, PythonSubprocessExecut
33
42
  exc_val: BaseException | None,
34
43
  exc_tb: TracebackType | None,
35
44
  ) -> None:
36
- return
45
+ """Async context manager exit: stop WebSocket listener."""
46
+ await self._stop_websocket_listener()
37
47
 
38
48
  async def arun(
39
49
  self,
@@ -76,6 +86,8 @@ class SubprocessWorkflowPublisher(LocalWorkflowPublisher, PythonSubprocessExecut
76
86
  publisher_name,
77
87
  "--published-workflow-file-name",
78
88
  published_workflow_file_name,
89
+ "--session-id",
90
+ self._session_id,
79
91
  ]
80
92
  if kwargs.get("pickle_control_flow_result"):
81
93
  args.append("--pickle-control-flow-result")
@@ -87,3 +99,10 @@ class SubprocessWorkflowPublisher(LocalWorkflowPublisher, PythonSubprocessExecut
87
99
  "GTN_CONFIG_ENABLE_WORKSPACE_FILE_WATCHING": "false",
88
100
  },
89
101
  )
102
+
103
+ async def _handle_subprocess_event(self, event: dict) -> None:
104
+ """Handle publisher-specific events from the subprocess.
105
+
106
+ Currently, this is a no-op as we just forward all events via the on_event callback.
107
+ Subclasses can override to add specific event handling logic.
108
+ """
@@ -1,7 +1,11 @@
1
1
  import asyncio
2
2
  import logging
3
3
  from argparse import ArgumentParser
4
+ from dataclasses import dataclass
4
5
 
6
+ from griptape_nodes.bootstrap.workflow_publishers.local_session_workflow_publisher import (
7
+ LocalSessionWorkflowPublisher,
8
+ )
5
9
  from griptape_nodes.bootstrap.workflow_publishers.local_workflow_publisher import LocalWorkflowPublisher
6
10
 
7
11
  logging.basicConfig(level=logging.INFO)
@@ -9,25 +13,35 @@ logging.basicConfig(level=logging.INFO)
9
13
  logger = logging.getLogger(__name__)
10
14
 
11
15
 
12
- async def _main(
13
- workflow_name: str,
14
- workflow_path: str,
15
- publisher_name: str,
16
- published_workflow_file_name: str,
17
- *,
18
- pickle_control_flow_result: bool,
19
- ) -> None:
20
- local_publisher = LocalWorkflowPublisher()
21
- async with local_publisher as publisher:
16
+ @dataclass
17
+ class PublishWorkflowArgs:
18
+ """Arguments for publishing a workflow."""
19
+
20
+ workflow_name: str
21
+ workflow_path: str
22
+ publisher_name: str
23
+ published_workflow_file_name: str
24
+ pickle_control_flow_result: bool
25
+ session_id: str | None = None
26
+
27
+
28
+ async def _main(args: PublishWorkflowArgs) -> None:
29
+ publisher: LocalWorkflowPublisher
30
+ if args.session_id is not None:
31
+ publisher = LocalSessionWorkflowPublisher(session_id=args.session_id)
32
+ else:
33
+ publisher = LocalWorkflowPublisher()
34
+
35
+ async with publisher:
22
36
  await publisher.arun(
23
- workflow_name=workflow_name,
24
- workflow_path=workflow_path,
25
- publisher_name=publisher_name,
26
- published_workflow_file_name=published_workflow_file_name,
27
- pickle_control_flow_result=pickle_control_flow_result,
37
+ workflow_name=args.workflow_name,
38
+ workflow_path=args.workflow_path,
39
+ publisher_name=args.publisher_name,
40
+ published_workflow_file_name=args.published_workflow_file_name,
41
+ pickle_control_flow_result=args.pickle_control_flow_result,
28
42
  )
29
43
 
30
- msg = f"Published workflow to file: {published_workflow_file_name}"
44
+ msg = f"Published workflow to file: {args.published_workflow_file_name}"
31
45
  logger.info(msg)
32
46
 
33
47
 
@@ -57,13 +71,19 @@ if __name__ == "__main__":
57
71
  default=False,
58
72
  help="Whether to pickle control flow results",
59
73
  )
60
- args = parser.parse_args()
61
- asyncio.run(
62
- _main(
63
- workflow_name=args.workflow_name,
64
- workflow_path=args.workflow_path,
65
- publisher_name=args.publisher_name,
66
- published_workflow_file_name=args.published_workflow_file_name,
67
- pickle_control_flow_result=args.pickle_control_flow_result,
68
- )
74
+ parser.add_argument(
75
+ "--session-id",
76
+ default=None,
77
+ help="Session ID for WebSocket event emission",
78
+ )
79
+ parsed_args = parser.parse_args()
80
+
81
+ publish_args = PublishWorkflowArgs(
82
+ workflow_name=parsed_args.workflow_name,
83
+ workflow_path=parsed_args.workflow_path,
84
+ publisher_name=parsed_args.publisher_name,
85
+ published_workflow_file_name=parsed_args.published_workflow_file_name,
86
+ pickle_control_flow_result=parsed_args.pickle_control_flow_result,
87
+ session_id=parsed_args.session_id,
69
88
  )
89
+ asyncio.run(_main(publish_args))
@@ -102,6 +102,8 @@ from griptape_nodes.retained_mode.managers.event_manager import (
102
102
  )
103
103
 
104
104
  if TYPE_CHECKING:
105
+ from collections.abc import Callable
106
+
105
107
  from griptape_nodes.retained_mode.events.node_events import SerializedNodeCommands
106
108
  from griptape_nodes.retained_mode.managers.library_manager import LibraryManager
107
109
 
@@ -220,6 +222,8 @@ class NodeExecutor:
220
222
  # Just execute the node normally! This means we aren't doing any special packaging.
221
223
  await node.aprocess()
222
224
  return
225
+ # Clear execution state before subprocess execution starts
226
+ node.subflow_execution_component.clear_execution_state()
223
227
  if execution_type == PRIVATE_EXECUTION:
224
228
  # Package the flow and run it in a subprocess.
225
229
  await self._execute_private_workflow(node)
@@ -251,7 +255,9 @@ class NodeExecutor:
251
255
  file_name: Name of workflow for logging
252
256
  package_result: The packaging result containing parameter mappings
253
257
  """
254
- my_subprocess_result = await self._execute_subprocess(workflow_path, file_name)
258
+ # Pass node for event updates if it's a SubflowNodeGroup
259
+ subflow_node = node if isinstance(node, SubflowNodeGroup) else None
260
+ my_subprocess_result = await self._execute_subprocess(workflow_path, file_name, node=subflow_node)
255
261
  parameter_output_values = self._extract_parameter_output_values(my_subprocess_result)
256
262
  self._apply_parameter_values_to_node(node, parameter_output_values, package_result)
257
263
 
@@ -343,7 +349,7 @@ class NodeExecutor:
343
349
 
344
350
  try:
345
351
  published_workflow_filename = await self._publish_library_workflow(
346
- workflow_result, library_name, result.file_name
352
+ workflow_result, library_name, result.file_name, node=node
347
353
  )
348
354
  except Exception as e:
349
355
  logger.exception(
@@ -481,19 +487,29 @@ class NodeExecutor:
481
487
  )
482
488
 
483
489
  async def _publish_library_workflow(
484
- self, workflow_result: SaveWorkflowFileFromSerializedFlowResultSuccess, library_name: str, file_name: str
490
+ self,
491
+ workflow_result: SaveWorkflowFileFromSerializedFlowResultSuccess,
492
+ library_name: str,
493
+ file_name: str,
494
+ node: BaseNode | None = None,
485
495
  ) -> Path:
486
- subprocess_workflow_publisher = SubprocessWorkflowPublisher()
496
+ # Define event callback if node is a SubflowNodeGroup for GUI updates
497
+ on_event: Callable[[dict], None] | None = None
498
+ if isinstance(node, SubflowNodeGroup):
499
+ on_event = node.subflow_execution_component.handle_publishing_event
500
+
501
+ subprocess_workflow_publisher = SubprocessWorkflowPublisher(on_event=on_event)
487
502
  published_filename = f"{Path(workflow_result.file_path).stem}_published"
488
503
  published_workflow_filename = GriptapeNodes.ConfigManager().workspace_path / (published_filename + ".py")
489
504
 
490
- await subprocess_workflow_publisher.arun(
491
- workflow_name=file_name,
492
- workflow_path=workflow_result.file_path,
493
- publisher_name=library_name,
494
- published_workflow_file_name=published_filename,
495
- pickle_control_flow_result=True,
496
- )
505
+ async with subprocess_workflow_publisher:
506
+ await subprocess_workflow_publisher.arun(
507
+ workflow_name=file_name,
508
+ workflow_path=workflow_result.file_path,
509
+ publisher_name=library_name,
510
+ published_workflow_file_name=published_filename,
511
+ pickle_control_flow_result=True,
512
+ )
497
513
 
498
514
  if not published_workflow_filename.exists():
499
515
  msg = f"Published workflow file does not exist at path: {published_workflow_filename}"
@@ -507,6 +523,7 @@ class NodeExecutor:
507
523
  file_name: str,
508
524
  pickle_control_flow_result: bool = True, # noqa: FBT001, FBT002
509
525
  flow_input: dict[str, Any] | None = None,
526
+ node: SubflowNodeGroup | None = None,
510
527
  ) -> dict[str, dict[str | SerializedNodeCommands.UniqueParameterValueUUID, Any] | None]:
511
528
  """Execute the published workflow in a subprocess.
512
529
 
@@ -515,6 +532,7 @@ class NodeExecutor:
515
532
  file_name: Name of the workflow for logging
516
533
  pickle_control_flow_result: Whether to pickle control flow results (defaults to True)
517
534
  flow_input: Optional dictionary of parameter values to pass to the workflow's StartFlow node
535
+ node: Optional SubflowNodeGroup to receive real-time event updates
518
536
 
519
537
  Returns:
520
538
  The subprocess execution output dictionary
@@ -523,7 +541,15 @@ class NodeExecutor:
523
541
  SubprocessWorkflowExecutor,
524
542
  )
525
543
 
526
- subprocess_executor = SubprocessWorkflowExecutor(workflow_path=str(published_workflow_filename))
544
+ # Define event callback if node provided for GUI updates
545
+ on_event: Callable[[dict], None] | None = None
546
+ if node is not None:
547
+ on_event = node.subflow_execution_component.handle_execution_event
548
+
549
+ subprocess_executor = SubprocessWorkflowExecutor(
550
+ workflow_path=str(published_workflow_filename),
551
+ on_event=on_event,
552
+ )
527
553
  try:
528
554
  async with subprocess_executor as executor:
529
555
  await executor.arun(
@@ -1454,6 +1480,10 @@ class NodeExecutor:
1454
1480
  # Get execution environment
1455
1481
  execution_type = node.get_parameter_value(node.execution_environment.name)
1456
1482
 
1483
+ # Clear execution state before subprocess execution starts (for non-local execution)
1484
+ if execution_type != LOCAL_EXECUTION:
1485
+ node.subflow_execution_component.clear_execution_state()
1486
+
1457
1487
  # Check if we should run in order (default is sequential/True)
1458
1488
  run_in_order = node.get_parameter_value("run_in_order")
1459
1489
 
@@ -2550,11 +2580,14 @@ class NodeExecutor:
2550
2580
  end_loop_node.name,
2551
2581
  )
2552
2582
 
2583
+ # Pass node for event updates if it's a SubflowNodeGroup (includes BaseIterativeNodeGroup)
2584
+ subflow_node = end_loop_node if isinstance(end_loop_node, SubflowNodeGroup) else None
2553
2585
  subprocess_result = await self._execute_subprocess(
2554
2586
  published_workflow_filename=workflow_path,
2555
2587
  file_name=f"{file_name_prefix}_iteration_{iteration_index}",
2556
2588
  pickle_control_flow_result=True,
2557
2589
  flow_input=flow_input,
2590
+ node=subflow_node,
2558
2591
  )
2559
2592
  iteration_outputs.append((iteration_index, True, subprocess_result))
2560
2593
  except Exception:
@@ -2562,6 +2595,9 @@ class NodeExecutor:
2562
2595
  iteration_outputs.append((iteration_index, False, None))
2563
2596
  else:
2564
2597
  # Execute all iterations concurrently
2598
+ # Get subflow_node reference for event updates (scoped outside the closure)
2599
+ subflow_node = end_loop_node if isinstance(end_loop_node, SubflowNodeGroup) else None
2600
+
2565
2601
  async def run_single_iteration(iteration_index: int) -> tuple[int, bool, dict[str, Any] | None]:
2566
2602
  try:
2567
2603
  flow_input = {start_node_name: parameter_values_per_iteration[iteration_index]}
@@ -2577,6 +2613,7 @@ class NodeExecutor:
2577
2613
  file_name=f"{file_name_prefix}_iteration_{iteration_index}",
2578
2614
  pickle_control_flow_result=True,
2579
2615
  flow_input=flow_input,
2616
+ node=subflow_node,
2580
2617
  )
2581
2618
  except Exception:
2582
2619
  logger.exception("Iteration %d failed for loop '%s'", iteration_index, end_loop_node.name)
@@ -2794,10 +2831,13 @@ class NodeExecutor:
2794
2831
  sanitized_loop_name = end_loop_node.name.replace(" ", "_")
2795
2832
  file_name_prefix = f"{sanitized_loop_name}_{library_name.replace(' ', '_')}_sequential_loop_flow"
2796
2833
 
2834
+ # Pass node for publishing progress events if it's a SubflowNodeGroup
2835
+ publish_node = end_loop_node if isinstance(end_loop_node, SubflowNodeGroup) else None
2797
2836
  published_workflow_filename, workflow_result = await self._publish_workflow_for_loop_execution(
2798
2837
  package_result=package_result,
2799
2838
  library_name=library_name,
2800
2839
  file_name=file_name_prefix,
2840
+ node=publish_node,
2801
2841
  )
2802
2842
 
2803
2843
  try:
@@ -2837,10 +2877,13 @@ class NodeExecutor:
2837
2877
  sanitized_loop_name = end_loop_node.name.replace(" ", "_")
2838
2878
  file_name_prefix = f"{sanitized_loop_name}_{library_name.replace(' ', '_')}_loop_flow"
2839
2879
 
2880
+ # Pass node for publishing progress events if it's a SubflowNodeGroup
2881
+ publish_node = end_loop_node if isinstance(end_loop_node, SubflowNodeGroup) else None
2840
2882
  published_workflow_filename, workflow_result = await self._publish_workflow_for_loop_execution(
2841
2883
  package_result=package_result,
2842
2884
  library_name=library_name,
2843
2885
  file_name=file_name_prefix,
2886
+ node=publish_node,
2844
2887
  )
2845
2888
 
2846
2889
  try:
@@ -2866,6 +2909,7 @@ class NodeExecutor:
2866
2909
  package_result: PackageNodesAsSerializedFlowResultSuccess,
2867
2910
  library_name: str,
2868
2911
  file_name: str,
2912
+ node: BaseNode | None = None,
2869
2913
  ) -> tuple[Path, Any]:
2870
2914
  """Save and publish workflow for loop execution via publisher.
2871
2915
 
@@ -2873,6 +2917,7 @@ class NodeExecutor:
2873
2917
  package_result: The packaged flow
2874
2918
  library_name: Name of the library to publish to
2875
2919
  file_name: Base file name for the workflow
2920
+ node: Optional node to receive publishing progress events
2876
2921
 
2877
2922
  Returns:
2878
2923
  Tuple of (published_workflow_filename, workflow_result)
@@ -2890,7 +2935,9 @@ class NodeExecutor:
2890
2935
  raise RuntimeError(msg) # noqa: TRY004 - This is a runtime failure, not a type validation error
2891
2936
 
2892
2937
  # Publish to the library
2893
- published_workflow_filename = await self._publish_library_workflow(workflow_result, library_name, file_name)
2938
+ published_workflow_filename = await self._publish_library_workflow(
2939
+ workflow_result, library_name, file_name, node=node
2940
+ )
2894
2941
 
2895
2942
  logger.info("Successfully published workflow to '%s'", published_workflow_filename)
2896
2943
 
@@ -139,7 +139,7 @@ class BaseIterativeStartNode(BaseNode):
139
139
  allowed_modes={ParameterMode.PROPERTY, ParameterMode.OUTPUT},
140
140
  settable=False,
141
141
  default_value=0,
142
- ui_options={"hide_property": True},
142
+ hide_property=True,
143
143
  )
144
144
  self.add_node_element(group)
145
145
 
@@ -16,6 +16,7 @@ from griptape_nodes.exe_types.node_types import (
16
16
  LOCAL_EXECUTION,
17
17
  get_library_names_with_publish_handlers,
18
18
  )
19
+ from griptape_nodes.exe_types.param_components.subflow_execution_component import SubflowExecutionComponent
19
20
  from griptape_nodes.retained_mode.events.connection_events import (
20
21
  CreateConnectionRequest,
21
22
  DeleteConnectionRequest,
@@ -84,6 +85,9 @@ class SubflowNodeGroup(BaseNodeGroup, ABC):
84
85
  # Add parameters from registered StartFlow nodes for each publishing library
85
86
  self._add_start_flow_parameters()
86
87
 
88
+ # Add subprocess execution status component for real-time GUI updates
89
+ self._add_subflow_execution_parameters()
90
+
87
91
  def _create_subflow(self) -> None:
88
92
  """Create a dedicated subflow for this NodeGroup's nodes.
89
93
 
@@ -144,6 +148,11 @@ class SubflowNodeGroup(BaseNodeGroup, ABC):
144
148
  for library_name, handler in event_handlers.items():
145
149
  self._process_library_start_flow_parameters(library_name, handler)
146
150
 
151
+ def _add_subflow_execution_parameters(self) -> None:
152
+ """Add parameters for subflow execution tracking."""
153
+ self._subflow_execution_component = SubflowExecutionComponent(self)
154
+ self._subflow_execution_component.add_output_parameters()
155
+
147
156
  def _process_library_start_flow_parameters(self, library_name: str, handler: Any) -> None:
148
157
  """Process and add StartFlow parameters from a single library.
149
158
 
@@ -649,6 +658,10 @@ class SubflowNodeGroup(BaseNodeGroup, ABC):
649
658
  self._cleanup_proxy_parameter(target_parameter, metadata_key)
650
659
  return super().after_incoming_connection_removed(source_node, source_parameter, target_parameter)
651
660
 
661
+ def after_value_set(self, parameter: Parameter, value: Any) -> None:
662
+ super().after_value_set(parameter, value)
663
+ self.subflow_execution_component.after_value_set(parameter, value)
664
+
652
665
  def add_nodes_to_group(self, nodes: list[BaseNode]) -> None:
653
666
  """Add nodes to the group and track their connections.
654
667
 
@@ -1004,3 +1017,8 @@ class SubflowNodeGroup(BaseNodeGroup, ABC):
1004
1017
  self.remove_nodes_from_group(nodes_to_remove)
1005
1018
  subflow_name = self.metadata.get("subflow_name")
1006
1019
  return subflow_name
1020
+
1021
+ @property
1022
+ def subflow_execution_component(self) -> SubflowExecutionComponent:
1023
+ """Get the subflow execution component for real-time status updates."""
1024
+ return self._subflow_execution_component
@@ -69,8 +69,7 @@ class StdoutCapture:
69
69
  self._original_stdout.flush()
70
70
 
71
71
  def isatty(self) -> bool:
72
- # Return False to prevent libraries from outputting ANSI color codes
73
- return False
72
+ return self._original_stdout.isatty()
74
73
 
75
74
  def __enter__(self) -> "StdoutCapture":
76
75
  sys.stdout = self