griptape-nodes 0.56.0__py3-none-any.whl → 0.57.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 (39) hide show
  1. griptape_nodes/app/app.py +10 -15
  2. griptape_nodes/app/watch.py +35 -67
  3. griptape_nodes/bootstrap/utils/__init__.py +1 -0
  4. griptape_nodes/bootstrap/utils/python_subprocess_executor.py +122 -0
  5. griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +418 -0
  6. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +37 -8
  7. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +326 -0
  8. griptape_nodes/bootstrap/workflow_executors/utils/__init__.py +1 -0
  9. griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +51 -0
  10. griptape_nodes/bootstrap/workflow_publishers/__init__.py +1 -0
  11. griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +43 -0
  12. griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +84 -0
  13. griptape_nodes/bootstrap/workflow_publishers/utils/__init__.py +1 -0
  14. griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +54 -0
  15. griptape_nodes/cli/commands/engine.py +4 -15
  16. griptape_nodes/cli/main.py +6 -1
  17. griptape_nodes/exe_types/core_types.py +26 -0
  18. griptape_nodes/exe_types/node_types.py +116 -1
  19. griptape_nodes/retained_mode/events/agent_events.py +2 -0
  20. griptape_nodes/retained_mode/events/base_events.py +18 -17
  21. griptape_nodes/retained_mode/events/execution_events.py +3 -1
  22. griptape_nodes/retained_mode/events/flow_events.py +5 -7
  23. griptape_nodes/retained_mode/events/mcp_events.py +363 -0
  24. griptape_nodes/retained_mode/events/node_events.py +3 -4
  25. griptape_nodes/retained_mode/griptape_nodes.py +8 -0
  26. griptape_nodes/retained_mode/managers/agent_manager.py +67 -4
  27. griptape_nodes/retained_mode/managers/event_manager.py +31 -13
  28. griptape_nodes/retained_mode/managers/flow_manager.py +76 -44
  29. griptape_nodes/retained_mode/managers/library_manager.py +7 -9
  30. griptape_nodes/retained_mode/managers/mcp_manager.py +364 -0
  31. griptape_nodes/retained_mode/managers/node_manager.py +12 -1
  32. griptape_nodes/retained_mode/managers/settings.py +40 -0
  33. griptape_nodes/retained_mode/managers/workflow_manager.py +94 -8
  34. griptape_nodes/traits/multi_options.py +5 -1
  35. griptape_nodes/traits/options.py +10 -2
  36. {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.57.0.dist-info}/METADATA +2 -2
  37. {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.57.0.dist-info}/RECORD +39 -26
  38. {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.57.0.dist-info}/WHEEL +0 -0
  39. {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.57.0.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import uuid
4
5
  from abc import ABC, abstractmethod
5
6
  from copy import deepcopy
@@ -9,6 +10,8 @@ from typing import TYPE_CHECKING, Any, ClassVar, Literal, NamedTuple, Self, Type
9
10
 
10
11
  from pydantic import BaseModel
11
12
 
13
+ logger = logging.getLogger("griptape_nodes")
14
+
12
15
 
13
16
  class NodeMessagePayload(BaseModel):
14
17
  """Structured payload for node messages.
@@ -554,6 +557,7 @@ class ParameterMessage(BaseNodeElement, UIOptionsMixin):
554
557
  button_align: ButtonAlignType = "full-width",
555
558
  full_width: bool = False,
556
559
  ui_options: dict | None = None,
560
+ traits: set[Trait.__class__ | Trait] | None = None,
557
561
  **kwargs,
558
562
  ):
559
563
  super().__init__(element_type=ParameterMessage.__name__, **kwargs)
@@ -569,6 +573,17 @@ class ParameterMessage(BaseNodeElement, UIOptionsMixin):
569
573
  self._full_width = full_width
570
574
  self._ui_options = ui_options or {}
571
575
 
576
+ # Handle traits if provided
577
+ if traits:
578
+ for trait in traits:
579
+ if isinstance(trait, type):
580
+ # It's a trait class, instantiate it
581
+ trait_instance = trait()
582
+ else:
583
+ # It's already a trait instance
584
+ trait_instance = trait
585
+ self.add_child(trait_instance)
586
+
572
587
  @property
573
588
  def variant(self) -> VariantType:
574
589
  return self._variant
@@ -694,6 +709,16 @@ class ParameterMessage(BaseNodeElement, UIOptionsMixin):
694
709
  else:
695
710
  button_icon = self.button_icon
696
711
 
712
+ # Check if there are any Button traits with on_click callbacks
713
+ has_button_callback = False
714
+ for child in self.children:
715
+ # Import here to avoid circular imports
716
+ from griptape_nodes.traits.button import Button
717
+
718
+ if isinstance(child, Button) and child.on_click_callback is not None:
719
+ has_button_callback = True
720
+ break
721
+
697
722
  # Merge the UI options with the message-specific options
698
723
  # Always include these fields, even if they're None or empty
699
724
  message_ui_options = {
@@ -705,6 +730,7 @@ class ParameterMessage(BaseNodeElement, UIOptionsMixin):
705
730
  "button_icon": button_icon,
706
731
  "button_variant": self.button_variant,
707
732
  "button_align": self.button_align,
733
+ "button_on_click": has_button_callback,
708
734
  "full_width": self.full_width,
709
735
  }
710
736
 
@@ -5,8 +5,9 @@ import logging
5
5
  from abc import ABC, abstractmethod
6
6
  from collections.abc import Callable, Generator, Iterable
7
7
  from concurrent.futures import ThreadPoolExecutor
8
+ from dataclasses import dataclass, field
8
9
  from enum import StrEnum, auto
9
- from typing import TYPE_CHECKING, Any, TypeVar
10
+ from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar
10
11
 
11
12
  from griptape_nodes.exe_types.core_types import (
12
13
  BaseNodeElement,
@@ -43,6 +44,7 @@ from griptape_nodes.traits.options import Options
43
44
 
44
45
  if TYPE_CHECKING:
45
46
  from griptape_nodes.exe_types.core_types import NodeMessagePayload
47
+ from griptape_nodes.node_library.library_registry import LibraryNameAndVersion
46
48
 
47
49
  logger = logging.getLogger("griptape_nodes")
48
50
 
@@ -50,6 +52,53 @@ T = TypeVar("T")
50
52
 
51
53
  AsyncResult = Generator[Callable[[], T], T]
52
54
 
55
+ LOCAL_EXECUTION = "Local Execution"
56
+
57
+
58
+ class ImportDependency(NamedTuple):
59
+ """Import dependency specification for a node.
60
+
61
+ Attributes:
62
+ module: The module name to import
63
+ class_name: Optional class name to import from the module. If None, imports the entire module.
64
+ """
65
+
66
+ module: str
67
+ class_name: str | None = None
68
+
69
+
70
+ @dataclass
71
+ class NodeDependencies:
72
+ """Dependencies that a node has on external resources.
73
+
74
+ This class provides a way for nodes to declare their dependencies on workflows,
75
+ static files, Python imports, and libraries. This information can be used by the system
76
+ for workflow packaging, dependency resolution, and deployment planning.
77
+
78
+ Attributes:
79
+ referenced_workflows: Set of workflow names that this node references
80
+ static_files: Set of static file names that this node depends on
81
+ imports: Set of Python imports that this node requires
82
+ libraries: Set of library names and versions that this node uses
83
+ """
84
+
85
+ referenced_workflows: set[str] = field(default_factory=set)
86
+ static_files: set[str] = field(default_factory=set)
87
+ imports: set[ImportDependency] = field(default_factory=set)
88
+ libraries: set[LibraryNameAndVersion] = field(default_factory=set)
89
+
90
+ def aggregate_from(self, other: NodeDependencies) -> None:
91
+ """Aggregate dependencies from another NodeDependencies object into this one.
92
+
93
+ Args:
94
+ other: The NodeDependencies object to aggregate from
95
+ """
96
+ # Aggregate all dependency types - no None checks needed since we use default_factory=set
97
+ self.referenced_workflows.update(other.referenced_workflows)
98
+ self.static_files.update(other.static_files)
99
+ self.imports.update(other.imports)
100
+ self.libraries.update(other.libraries)
101
+
53
102
 
54
103
  class NodeResolutionState(StrEnum):
55
104
  """Possible states for a node during resolution."""
@@ -59,6 +108,23 @@ class NodeResolutionState(StrEnum):
59
108
  RESOLVED = auto()
60
109
 
61
110
 
111
+ def get_library_names_with_publish_handlers() -> list[str]:
112
+ """Get names of all registered libraries that have PublishWorkflowRequest handlers."""
113
+ from griptape_nodes.retained_mode.events.workflow_events import PublishWorkflowRequest
114
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
115
+
116
+ library_manager = GriptapeNodes.LibraryManager()
117
+ event_handlers = library_manager.get_registered_event_handlers(PublishWorkflowRequest)
118
+
119
+ # Always include "local" as the first option
120
+ library_names = [LOCAL_EXECUTION]
121
+
122
+ # Add all registered library names that can handle PublishWorkflowRequest
123
+ library_names.extend(sorted(event_handlers.keys()))
124
+
125
+ return library_names
126
+
127
+
62
128
  class BaseNode(ABC):
63
129
  # Owned by a flow
64
130
  name: str
@@ -104,6 +170,16 @@ class BaseNode(ABC):
104
170
  self.process_generator = None
105
171
  self._tracked_parameters = []
106
172
  self.set_entry_control_parameter(None)
173
+ self.execution_environment = Parameter(
174
+ name="execution_environment",
175
+ tooltip="Environment that the node should execute in",
176
+ type=ParameterTypeBuiltin.STR,
177
+ allowed_modes={ParameterMode.PROPERTY},
178
+ default_value=LOCAL_EXECUTION,
179
+ traits={Options(choices=get_library_names_with_publish_handlers())},
180
+ ui_options={"hide": True},
181
+ )
182
+ self.add_parameter(self.execution_environment)
107
183
 
108
184
  # This is gross and we need to have a universal pass on resolution state changes and emission of events. That's what this ticket does!
109
185
  # https://github.com/griptape-ai/griptape-nodes/issues/994
@@ -819,6 +895,35 @@ class BaseNode(ABC):
819
895
  # Then clear the reference to the first spotlight parameter
820
896
  self.current_spotlight_parameter = None
821
897
 
898
+ def get_node_dependencies(self) -> NodeDependencies | None:
899
+ """Return the dependencies that this node has on external resources.
900
+
901
+ This method should be overridden by nodes that have dependencies on:
902
+ - Referenced workflows: Other workflows that this node calls or references
903
+ - Static files: Files that this node reads from or requires for operation
904
+ - Python imports: Modules or classes that this node imports beyond standard dependencies
905
+
906
+ This information can be used by the system for workflow packaging, dependency
907
+ resolution, deployment planning, and ensuring all required resources are available.
908
+
909
+ Returns:
910
+ NodeDependencies object containing the node's dependencies, or None if the node
911
+ has no external dependencies beyond the standard framework dependencies.
912
+
913
+ Example:
914
+ def get_node_dependencies(self) -> NodeDependencies | None:
915
+ return NodeDependencies(
916
+ referenced_workflows={"image_processing_workflow", "validation_workflow"},
917
+ static_files={"config.json", "model_weights.pkl"},
918
+ imports={
919
+ ImportDependency("numpy"),
920
+ ImportDependency("sklearn.linear_model", "LinearRegression"),
921
+ ImportDependency("custom_module", "SpecialProcessor")
922
+ }
923
+ )
924
+ """
925
+ return None
926
+
822
927
  def append_value_to_parameter(self, parameter_name: str, value: Any) -> None:
823
928
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
824
929
 
@@ -1390,6 +1495,16 @@ class EndNode(BaseNode):
1390
1495
 
1391
1496
  self.status_component.set_execution_result(was_successful=was_successful, result_details=details)
1392
1497
 
1498
+ # Update all values to use the output value
1499
+ for param in self.parameters:
1500
+ if param.type != ParameterTypeBuiltin.CONTROL_TYPE:
1501
+ value = self.get_parameter_value(param.name)
1502
+ self.parameter_output_values[param.name] = value
1503
+ next_control_output = self.get_next_control_output()
1504
+ # Update which control parameter to flag as the output value.
1505
+ if next_control_output is not None:
1506
+ self.parameter_output_values[next_control_output.name] = 1
1507
+
1393
1508
 
1394
1509
  class StartLoopNode(BaseNode):
1395
1510
  end_node: EndLoopNode | None = None
@@ -29,12 +29,14 @@ class RunAgentRequest(RequestPayload):
29
29
  Args:
30
30
  input: Text input to send to the agent
31
31
  url_artifacts: List of URL artifacts to include with the request
32
+ additional_mcp_servers: List of additional MCP server names to include
32
33
 
33
34
  Results: RunAgentResultStarted -> RunAgentResultSuccess (with output) | RunAgentResultFailure (execution error)
34
35
  """
35
36
 
36
37
  input: str
37
38
  url_artifacts: list[RunAgentRequestArtifact]
39
+ additional_mcp_servers: list[str] = field(default_factory=list)
38
40
 
39
41
 
40
42
  @dataclass
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import logging
5
5
  from abc import ABC, abstractmethod
6
- from dataclasses import asdict, dataclass, field, is_dataclass
6
+ from dataclasses import asdict, dataclass, field, fields, is_dataclass
7
7
  from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
8
8
 
9
9
  from griptape.artifacts import BaseArtifact
@@ -389,6 +389,21 @@ class EventResult[P: RequestPayload, R: ResultPayload](BaseEvent, ABC):
389
389
  bool: True if success, False if failure
390
390
  """
391
391
 
392
+ @classmethod
393
+ def _create_payload_instance(cls, payload_type: type, payload_data: dict[str, Any]) -> Any:
394
+ """Create a payload instance from data, handling dataclass init=False fields."""
395
+ if is_dataclass(payload_type):
396
+ # Filter out fields that have init=False to avoid TypeError
397
+ init_fields = {f.name for f in fields(payload_type) if f.init}
398
+ filtered_data = {k: v for k, v in payload_data.items() if k in init_fields}
399
+ return payload_type(**filtered_data)
400
+ if issubclass(payload_type, BaseModel):
401
+ return payload_type.model_validate(payload_data)
402
+ instance = payload_type()
403
+ for key, value in payload_data.items():
404
+ setattr(instance, key, value)
405
+ return instance
406
+
392
407
  @classmethod
393
408
  def from_dict( # pyright: ignore[reportIncompatibleMethodOverride]
394
409
  cls, data: builtins.dict[str, Any], req_payload_type: type[P], res_payload_type: type[R]
@@ -403,28 +418,14 @@ class EventResult[P: RequestPayload, R: ResultPayload](BaseEvent, ABC):
403
418
 
404
419
  # Process request payload
405
420
  if req_payload_type:
406
- if is_dataclass(req_payload_type):
407
- request_payload = req_payload_type(**request_data)
408
- elif issubclass(req_payload_type, BaseModel):
409
- request_payload = req_payload_type.model_validate(request_data)
410
- else:
411
- request_payload = req_payload_type()
412
- for key, value in request_data.items():
413
- setattr(request_payload, key, value)
421
+ request_payload = cls._create_payload_instance(req_payload_type, request_data)
414
422
  else:
415
423
  msg = f"Cannot create {cls.__name__} without a request payload type"
416
424
  raise ValueError(msg)
417
425
 
418
426
  # Process result payload
419
427
  if res_payload_type:
420
- if is_dataclass(res_payload_type):
421
- result_payload = res_payload_type(**result_data)
422
- elif issubclass(res_payload_type, BaseModel):
423
- result_payload = res_payload_type.model_validate(result_data)
424
- else:
425
- result_payload = res_payload_type()
426
- for key, value in result_data.items():
427
- setattr(result_payload, key, value)
428
+ result_payload = cls._create_payload_instance(res_payload_type, result_data)
428
429
  else:
429
430
  msg = f"Cannot create {cls.__name__} without a result payload type"
430
431
  raise ValueError(msg)
@@ -4,6 +4,7 @@ from typing import Any
4
4
  from griptape_nodes.retained_mode.events.base_events import (
5
5
  ExecutionPayload,
6
6
  RequestPayload,
7
+ ResultDetails,
7
8
  ResultPayloadFailure,
8
9
  ResultPayloadSuccess,
9
10
  WorkflowAlteredMixin,
@@ -307,7 +308,8 @@ class ControlFlowResolvedEvent(ExecutionPayload):
307
308
  @dataclass
308
309
  @PayloadRegistry.register
309
310
  class ControlFlowCancelledEvent(ExecutionPayload):
310
- pass
311
+ result_details: ResultDetails | str | None = None
312
+ exception: Exception | None = None
311
313
 
312
314
 
313
315
  @dataclass
@@ -1,7 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import Any
3
3
 
4
- from griptape_nodes.node_library.library_registry import LibraryNameAndVersion
4
+ from griptape_nodes.exe_types.node_types import NodeDependencies
5
5
  from griptape_nodes.node_library.workflow_registry import WorkflowShape
6
6
  from griptape_nodes.retained_mode.events.base_events import (
7
7
  RequestPayload,
@@ -186,8 +186,6 @@ class SerializedFlowCommands:
186
186
  Useful for save/load, copy/paste, etc.
187
187
 
188
188
  Attributes:
189
- node_libraries_used (set[LibraryNameAndVersion]): Set of libraries and versions used by the nodes,
190
- including those in child flows.
191
189
  flow_initialization_command (CreateFlowRequest | ImportWorkflowAsReferencedSubFlowRequest | None): Command to initialize the flow that contains all of this.
192
190
  Can be CreateFlowRequest for standalone flows, ImportWorkflowAsReferencedSubFlowRequest for referenced workflows,
193
191
  or None to deserialize into whatever Flow is in the Current Context.
@@ -200,8 +198,9 @@ class SerializedFlowCommands:
200
198
  set_parameter_value_commands (dict[SerializedNodeCommands.NodeUUID, list[SerializedNodeCommands.IndirectSetParameterValueCommand]]): List of commands
201
199
  to set parameter values, keyed by node UUID, during deserialization.
202
200
  sub_flows_commands (list["SerializedFlowCommands"]): List of sub-flow commands. Cascades into sub-flows within this serialization.
203
- referenced_workflows (set[str]): Set of workflow file paths that are referenced by this flow and its sub-flows.
204
- Used for validation before deserialization to ensure all referenced workflows are available.
201
+ node_dependencies (NodeDependencies): Aggregated dependencies from all nodes in this flow and its sub-flows.
202
+ Includes referenced workflows, static files, Python imports, and libraries. Used for workflow packaging,
203
+ dependency resolution, and deployment planning.
205
204
  """
206
205
 
207
206
  @dataclass
@@ -222,7 +221,6 @@ class SerializedFlowCommands:
222
221
  target_node_uuid: SerializedNodeCommands.NodeUUID
223
222
  target_parameter_name: str
224
223
 
225
- node_libraries_used: set[LibraryNameAndVersion]
226
224
  flow_initialization_command: CreateFlowRequest | ImportWorkflowAsReferencedSubFlowRequest | None
227
225
  serialized_node_commands: list[SerializedNodeCommands]
228
226
  serialized_connections: list[IndirectConnectionSerialization]
@@ -232,7 +230,7 @@ class SerializedFlowCommands:
232
230
  ]
233
231
  set_lock_commands_per_node: dict[SerializedNodeCommands.NodeUUID, SetLockNodeStateRequest]
234
232
  sub_flows_commands: list["SerializedFlowCommands"]
235
- referenced_workflows: set[str]
233
+ node_dependencies: NodeDependencies
236
234
 
237
235
 
238
236
  @dataclass