griptape-nodes 0.59.3__py3-none-any.whl → 0.60.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 (41) hide show
  1. griptape_nodes/common/macro_parser/__init__.py +28 -0
  2. griptape_nodes/common/macro_parser/core.py +230 -0
  3. griptape_nodes/common/macro_parser/exceptions.py +23 -0
  4. griptape_nodes/common/macro_parser/formats.py +170 -0
  5. griptape_nodes/common/macro_parser/matching.py +134 -0
  6. griptape_nodes/common/macro_parser/parsing.py +172 -0
  7. griptape_nodes/common/macro_parser/resolution.py +168 -0
  8. griptape_nodes/common/macro_parser/segments.py +42 -0
  9. griptape_nodes/exe_types/core_types.py +241 -4
  10. griptape_nodes/exe_types/node_types.py +7 -1
  11. griptape_nodes/exe_types/param_components/huggingface/__init__.py +1 -0
  12. griptape_nodes/exe_types/param_components/huggingface/huggingface_model_parameter.py +168 -0
  13. griptape_nodes/exe_types/param_components/huggingface/huggingface_repo_file_parameter.py +38 -0
  14. griptape_nodes/exe_types/param_components/huggingface/huggingface_repo_parameter.py +33 -0
  15. griptape_nodes/exe_types/param_components/huggingface/huggingface_utils.py +136 -0
  16. griptape_nodes/exe_types/param_components/log_parameter.py +136 -0
  17. griptape_nodes/exe_types/param_components/seed_parameter.py +59 -0
  18. griptape_nodes/exe_types/param_types/__init__.py +1 -0
  19. griptape_nodes/exe_types/param_types/parameter_bool.py +221 -0
  20. griptape_nodes/exe_types/param_types/parameter_float.py +179 -0
  21. griptape_nodes/exe_types/param_types/parameter_int.py +183 -0
  22. griptape_nodes/exe_types/param_types/parameter_number.py +380 -0
  23. griptape_nodes/exe_types/param_types/parameter_string.py +232 -0
  24. griptape_nodes/node_library/library_registry.py +2 -1
  25. griptape_nodes/retained_mode/events/app_events.py +21 -0
  26. griptape_nodes/retained_mode/events/os_events.py +142 -6
  27. griptape_nodes/retained_mode/events/parameter_events.py +2 -0
  28. griptape_nodes/retained_mode/griptape_nodes.py +14 -0
  29. griptape_nodes/retained_mode/managers/agent_manager.py +5 -3
  30. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +19 -1
  31. griptape_nodes/retained_mode/managers/library_manager.py +8 -1
  32. griptape_nodes/retained_mode/managers/node_manager.py +14 -1
  33. griptape_nodes/retained_mode/managers/os_manager.py +403 -124
  34. griptape_nodes/retained_mode/managers/user_manager.py +120 -0
  35. griptape_nodes/retained_mode/managers/workflow_manager.py +44 -34
  36. griptape_nodes/traits/multi_options.py +26 -2
  37. griptape_nodes/utils/huggingface_utils.py +136 -0
  38. {griptape_nodes-0.59.3.dist-info → griptape_nodes-0.60.0.dist-info}/METADATA +1 -1
  39. {griptape_nodes-0.59.3.dist-info → griptape_nodes-0.60.0.dist-info}/RECORD +41 -18
  40. {griptape_nodes-0.59.3.dist-info → griptape_nodes-0.60.0.dist-info}/WHEEL +1 -1
  41. {griptape_nodes-0.59.3.dist-info → griptape_nodes-0.60.0.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  from dataclasses import dataclass
2
+ from enum import StrEnum
2
3
 
3
4
  from griptape_nodes.retained_mode.events.base_events import (
4
5
  RequestPayload,
@@ -9,6 +10,43 @@ from griptape_nodes.retained_mode.events.base_events import (
9
10
  from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
10
11
 
11
12
 
13
+ class ExistingFilePolicy(StrEnum):
14
+ """Policy for handling existing files during write operations."""
15
+
16
+ OVERWRITE = "overwrite" # Replace existing file content
17
+ FAIL = "fail" # Fail if file exists
18
+ CREATE_NEW = "create_new" # Create new file with modified name (e.g., file_1.txt)
19
+
20
+
21
+ class FileIOFailureReason(StrEnum):
22
+ """Classification of file I/O failure reasons.
23
+
24
+ Used by read and write operations to provide structured error information.
25
+ """
26
+
27
+ # Policy violations
28
+ POLICY_NO_OVERWRITE = "policy_no_overwrite" # File exists and policy prohibits overwrite
29
+ POLICY_NO_CREATE_PARENT_DIRS = "policy_no_create_parent_dirs" # Parent dir missing and policy prohibits creation
30
+
31
+ # Permission/access errors
32
+ PERMISSION_DENIED = "permission_denied" # No read/write permission
33
+ FILE_NOT_FOUND = "file_not_found" # File doesn't exist (read operations)
34
+
35
+ # Resource errors
36
+ DISK_FULL = "disk_full" # Insufficient disk space
37
+
38
+ # Path errors
39
+ INVALID_PATH = "invalid_path" # Malformed or invalid path
40
+ IS_DIRECTORY = "is_directory" # Path is a directory, not a file
41
+
42
+ # Content errors
43
+ ENCODING_ERROR = "encoding_error" # Text encoding/decoding failed
44
+
45
+ # Generic errors
46
+ IO_ERROR = "io_error" # Generic I/O error
47
+ UNKNOWN = "unknown" # Unexpected error
48
+
49
+
12
50
  @dataclass
13
51
  class FileSystemEntry:
14
52
  """Represents a file or directory in the file system."""
@@ -50,7 +88,14 @@ class OpenAssociatedFileResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSucc
50
88
  @dataclass
51
89
  @PayloadRegistry.register
52
90
  class OpenAssociatedFileResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
53
- """File or directory opening failed. Common causes: path not found, no associated application, permission denied."""
91
+ """File or directory opening failed.
92
+
93
+ Attributes:
94
+ failure_reason: Classification of why the open failed
95
+ result_details: Human-readable error message (inherited from ResultPayloadFailure)
96
+ """
97
+
98
+ failure_reason: FileIOFailureReason
54
99
 
55
100
 
56
101
  @dataclass
@@ -88,7 +133,14 @@ class ListDirectoryResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
88
133
  @dataclass
89
134
  @PayloadRegistry.register
90
135
  class ListDirectoryResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
91
- """Directory listing failed. Common causes: access denied, path not found."""
136
+ """Directory listing failed.
137
+
138
+ Attributes:
139
+ failure_reason: Classification of why the listing failed
140
+ result_details: Human-readable error message (inherited from ResultPayloadFailure)
141
+ """
142
+
143
+ failure_reason: FileIOFailureReason
92
144
 
93
145
 
94
146
  @dataclass
@@ -105,6 +157,7 @@ class ReadFileRequest(RequestPayload):
105
157
  encoding: Text encoding to use if file is detected as text (default: 'utf-8')
106
158
  workspace_only: If True, constrain to workspace directory. If False, allow system-wide access.
107
159
  If None, workspace constraints don't apply (e.g., cloud environments).
160
+ TODO: Remove workspace_only parameter - see https://github.com/griptape-ai/griptape-nodes/issues/2753
108
161
 
109
162
  Results: ReadFileResultSuccess (with content) | ReadFileResultFailure (file not found, permission denied)
110
163
  """
@@ -112,7 +165,7 @@ class ReadFileRequest(RequestPayload):
112
165
  file_path: str | None = None
113
166
  file_entry: FileSystemEntry | None = None
114
167
  encoding: str = "utf-8"
115
- workspace_only: bool | None = True
168
+ workspace_only: bool | None = True # TODO: Remove - see https://github.com/griptape-ai/griptape-nodes/issues/2753
116
169
 
117
170
 
118
171
  @dataclass
@@ -139,7 +192,14 @@ class ReadFileResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
139
192
  @dataclass
140
193
  @PayloadRegistry.register
141
194
  class ReadFileResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
142
- """File reading failed. Common causes: file not found, permission denied, encoding error."""
195
+ """File reading failed.
196
+
197
+ Attributes:
198
+ failure_reason: Classification of why the read failed
199
+ result_details: Human-readable error message (inherited from ResultPayloadFailure)
200
+ """
201
+
202
+ failure_reason: FileIOFailureReason
143
203
 
144
204
 
145
205
  @dataclass
@@ -193,7 +253,14 @@ class CreateFileResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
193
253
  @dataclass
194
254
  @PayloadRegistry.register
195
255
  class CreateFileResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
196
- """File/directory creation failed."""
256
+ """File/directory creation failed.
257
+
258
+ Attributes:
259
+ failure_reason: Classification of why the creation failed
260
+ result_details: Human-readable error message (inherited from ResultPayloadFailure)
261
+ """
262
+
263
+ failure_reason: FileIOFailureReason
197
264
 
198
265
 
199
266
  @dataclass
@@ -229,4 +296,73 @@ class RenameFileResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
229
296
  @dataclass
230
297
  @PayloadRegistry.register
231
298
  class RenameFileResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
232
- """File/directory rename failed."""
299
+ """File/directory rename failed.
300
+
301
+ Attributes:
302
+ failure_reason: Classification of why the rename failed
303
+ result_details: Human-readable error message (inherited from ResultPayloadFailure)
304
+ """
305
+
306
+ failure_reason: FileIOFailureReason
307
+
308
+
309
+ @dataclass
310
+ @PayloadRegistry.register
311
+ class WriteFileRequest(RequestPayload):
312
+ """Write content to a file.
313
+
314
+ Automatically detects text vs binary mode based on content type.
315
+
316
+ Use when: Saving generated content, writing output files,
317
+ creating configuration files, writing binary data.
318
+
319
+ Args:
320
+ file_path: Path to the file to write
321
+ content: Content to write (str for text files, bytes for binary files)
322
+ encoding: Text encoding for str content (default: 'utf-8', ignored for bytes)
323
+ append: If True, append to existing file; if False, use existing_file_policy (default: False)
324
+ existing_file_policy: How to handle existing files when append=False:
325
+ - "overwrite": Replace file content (default)
326
+ - "fail": Return failure if file exists
327
+ - "create_new": Create new file with modified name (NOT YET IMPLEMENTED)
328
+ create_parents: If True, create parent directories if missing (default: True)
329
+
330
+ Results: WriteFileResultSuccess | WriteFileResultFailure
331
+
332
+ Note: existing_file_policy is ignored when append=True (append always allows existing files)
333
+ """
334
+
335
+ file_path: str
336
+ content: str | bytes
337
+ encoding: str = "utf-8" # Ignored for bytes
338
+ append: bool = False
339
+ existing_file_policy: ExistingFilePolicy = ExistingFilePolicy.OVERWRITE
340
+ create_parents: bool = True
341
+
342
+
343
+ @dataclass
344
+ @PayloadRegistry.register
345
+ class WriteFileResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
346
+ """File written successfully.
347
+
348
+ Attributes:
349
+ final_file_path: The actual path where file was written
350
+ (may differ from requested path if create_new policy used)
351
+ bytes_written: Number of bytes written to the file
352
+ """
353
+
354
+ final_file_path: str
355
+ bytes_written: int
356
+
357
+
358
+ @dataclass
359
+ @PayloadRegistry.register
360
+ class WriteFileResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
361
+ """File write failed.
362
+
363
+ Attributes:
364
+ failure_reason: Classification of why the write failed
365
+ result_details: Human-readable error message (inherited from ResultPayloadFailure)
366
+ """
367
+
368
+ failure_reason: FileIOFailureReason
@@ -47,6 +47,7 @@ class AddParameterToNodeRequest(RequestPayload):
47
47
  mode_allowed_output: Whether parameter can be used as output
48
48
  is_user_defined: Whether this is a user-defined parameter (affects serialization)
49
49
  parent_container_name: Name of parent container if nested
50
+ parent_element_name: Name of parent element if nested
50
51
  initial_setup: Skip setup work when loading from file
51
52
  settable: Whether parameter can be set directly by the user or not
52
53
 
@@ -71,6 +72,7 @@ class AddParameterToNodeRequest(RequestPayload):
71
72
  is_user_defined: bool = Field(default=True)
72
73
  settable: bool = Field(default=True)
73
74
  parent_container_name: str | None = None
75
+ parent_element_name: str | None = None
74
76
  # initial_setup prevents unnecessary work when we are loading a workflow from a file.
75
77
  initial_setup: bool = False
76
78
 
@@ -58,6 +58,7 @@ if TYPE_CHECKING:
58
58
  StaticFilesManager,
59
59
  )
60
60
  from griptape_nodes.retained_mode.managers.sync_manager import SyncManager
61
+ from griptape_nodes.retained_mode.managers.user_manager import UserManager
61
62
  from griptape_nodes.retained_mode.managers.variable_manager import (
62
63
  VariablesManager,
63
64
  )
@@ -93,6 +94,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
93
94
  _mcp_manager: MCPManager
94
95
  _resource_manager: ResourceManager
95
96
  _sync_manager: SyncManager
97
+ _user_manager: UserManager
96
98
 
97
99
  def __init__(self) -> None:
98
100
  from griptape_nodes.retained_mode.managers.agent_manager import AgentManager
@@ -120,6 +122,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
120
122
  StaticFilesManager,
121
123
  )
122
124
  from griptape_nodes.retained_mode.managers.sync_manager import SyncManager
125
+ from griptape_nodes.retained_mode.managers.user_manager import UserManager
123
126
  from griptape_nodes.retained_mode.managers.variable_manager import (
124
127
  VariablesManager,
125
128
  )
@@ -156,6 +159,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
156
159
  self._session_manager = SessionManager(self._engine_identity_manager, self._event_manager)
157
160
  self._mcp_manager = MCPManager(self._event_manager, self._config_manager)
158
161
  self._sync_manager = SyncManager(self._event_manager, self._config_manager)
162
+ self._user_manager = UserManager(self._secrets_manager)
159
163
 
160
164
  # Assign handlers now that these are created.
161
165
  self._event_manager.assign_manager_to_request_type(
@@ -322,6 +326,10 @@ class GriptapeNodes(metaclass=SingletonMeta):
322
326
  def VariablesManager(cls) -> VariablesManager:
323
327
  return GriptapeNodes.get_instance()._workflow_variables_manager
324
328
 
329
+ @classmethod
330
+ def UserManager(cls) -> UserManager:
331
+ return GriptapeNodes.get_instance()._user_manager
332
+
325
333
  @classmethod
326
334
  def clear_data(cls) -> None:
327
335
  # Get canvas
@@ -374,6 +382,10 @@ class GriptapeNodes(metaclass=SingletonMeta):
374
382
  # Get engine name
375
383
  engine_name = GriptapeNodes.EngineIdentityManager().engine_name
376
384
 
385
+ # Get user and organization
386
+ user = GriptapeNodes.UserManager().user
387
+ user_organization = GriptapeNodes.UserManager().user_organization
388
+
377
389
  return EngineHeartbeatResultSuccess(
378
390
  heartbeat_id=request.heartbeat_id,
379
391
  engine_version=engine_version,
@@ -381,6 +393,8 @@ class GriptapeNodes(metaclass=SingletonMeta):
381
393
  engine_id=GriptapeNodes.EngineIdentityManager().active_engine_id,
382
394
  session_id=GriptapeNodes.SessionManager().active_session_id,
383
395
  timestamp=datetime.now(tz=UTC).isoformat(),
396
+ user=user,
397
+ user_organization=user_organization,
384
398
  result_details="Engine heartbeat successful",
385
399
  **instance_info,
386
400
  **workflow_info,
@@ -208,8 +208,8 @@ class AgentManager:
208
208
  def _create_agent(self, additional_mcp_servers: list[str] | None = None) -> Agent:
209
209
  output_schema = Schema(
210
210
  {
211
- "generated_image_urls": [str],
212
211
  "conversation_output": str,
212
+ "generated_image_urls": [str],
213
213
  }
214
214
  )
215
215
 
@@ -249,10 +249,11 @@ class AgentManager:
249
249
  if url_artifact["type"] == "ImageUrlArtifact"
250
250
  ]
251
251
  agent = self._create_agent(additional_mcp_servers=request.additional_mcp_servers)
252
- *events, last_event = agent.run_stream([request.input, *artifacts])
252
+ event_stream = agent.run_stream([request.input, *artifacts])
253
253
  full_result = ""
254
254
  last_conversation_output = ""
255
- for event in events:
255
+ last_event = None
256
+ for event in event_stream:
256
257
  if isinstance(event, TextChunkEvent):
257
258
  full_result += event.token
258
259
  try:
@@ -273,6 +274,7 @@ class AgentManager:
273
274
  last_conversation_output = new_conversation_output
274
275
  except json.JSONDecodeError:
275
276
  pass # Ignore incomplete JSON
277
+ last_event = event
276
278
  if isinstance(last_event, FinishTaskEvent):
277
279
  if isinstance(last_event.task_output, ErrorArtifact):
278
280
  return RunAgentResultFailure(
@@ -40,7 +40,25 @@ class ArbitraryCodeExecManager:
40
40
  try:
41
41
  string_buffer = io.StringIO()
42
42
  with redirect_stdout(string_buffer):
43
- python_output = exec(request.python_string) # noqa: S102
43
+ # Use a shared namespace for both globals and locals in exec() to make some behavior possible and more intuitive:
44
+ #
45
+ # 1. RECURSION: Without this namespace, recursive functions defined inside exec() fail with
46
+ # "NameError: name 'function_name' is not defined" when they try to call themselves.
47
+ # Why? When exec() runs with default parameters, functions defined in the exec'd code
48
+ # exist in this method's local scope. But inside the exec'd functions, Python looks in the program's
49
+ # global scope (outside this method) and the function's own local scope - neither of which
50
+ # contains the recursive function definition. By passing the same dict as both globals and locals,
51
+ # any function defined in exec'd code becomes visible in what exec'd code sees as
52
+ # "global" scope, allowing recursive calls to find the function definition.
53
+ #
54
+ # 2. ISOLATION: An isolated namespace prevents exec'd code from accessing or modifying
55
+ # variables in the outer program scope, protecting read/write access to sensitive engine data.
56
+ # For the PR that implements this behavior alongside an Execute Python and List Files node, see https://github.com/griptape-ai/griptape-nodes/pull/2087
57
+
58
+ namespace = {"__builtins__": __builtins__}
59
+ python_output = exec( # noqa: S102
60
+ request.python_string, namespace, namespace
61
+ )
44
62
 
45
63
  captured_output = strip_ansi_codes(string_buffer.getvalue())
46
64
  result = RunArbitraryPythonStringResultSuccess(
@@ -1592,6 +1592,13 @@ class LibraryManager:
1592
1592
  session_id = GriptapeNodes.get_session_id()
1593
1593
  session_info = f" | Session: {session_id[:8]}..." if session_id else " | No Session"
1594
1594
 
1595
+ # Get user and organization
1596
+ user = GriptapeNodes.UserManager().user
1597
+ user_info = f" | User: {user.email if user else 'Not available'}"
1598
+
1599
+ user_organization = GriptapeNodes.UserManager().user_organization
1600
+ org_info = f" | Org: {user_organization.name if user_organization else 'Not available'}"
1601
+
1595
1602
  nodes_app_url = os.getenv("GRIPTAPE_NODES_UI_BASE_URL", "https://nodes.griptape.ai")
1596
1603
  message = Panel(
1597
1604
  Align.center(
@@ -1600,7 +1607,7 @@ class LibraryManager:
1600
1607
  vertical="middle",
1601
1608
  ),
1602
1609
  title="Griptape Nodes Engine Started",
1603
- subtitle=f"[green]{engine_version}{session_info}[/green]",
1610
+ subtitle=f"[green]Version: {engine_version}{session_info}{user_info}{org_info}[/green]",
1604
1611
  border_style="green",
1605
1612
  padding=(1, 4),
1606
1613
  )
@@ -918,6 +918,7 @@ class NodeManager:
918
918
  def on_add_parameter_to_node_request(self, request: AddParameterToNodeRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912, PLR0915
919
919
  node_name = request.node_name
920
920
  node = None
921
+ parent_group: ParameterGroup | None = None
921
922
 
922
923
  if node_name is None:
923
924
  # Get from the current context.
@@ -968,6 +969,15 @@ class NodeManager:
968
969
  node_name=node_name,
969
970
  result_details=f"Successfully added parameter '{new_param.name}' to container parameter '{request.parent_container_name}' in node '{node_name}'.",
970
971
  )
972
+ if request.parent_element_name is not None:
973
+ parent_element = node.get_element_by_name_and_type(request.parent_element_name)
974
+ if parent_element is None:
975
+ details = f"Attempted to add Parameter to Parent Element '{request.parent_element_name}' in node '{node_name}'. Failed because element didn't exist."
976
+ result = AddParameterToNodeResultFailure(result_details=details)
977
+ return result
978
+ # Handle ParameterGroup parentage with potential to expand in future to other element types.
979
+ if isinstance(parent_element, ParameterGroup):
980
+ parent_group = parent_element
971
981
  if request.parameter_name is None or request.tooltip is None:
972
982
  details = f"Attempted to add Parameter to node '{node_name}'. Failed because default_value, tooltip, or parameter_name was not defined."
973
983
  result = AddParameterToNodeResultFailure(result_details=details)
@@ -1032,6 +1042,7 @@ class NodeManager:
1032
1042
  allowed_modes=allowed_modes,
1033
1043
  ui_options=request.ui_options,
1034
1044
  parent_container_name=request.parent_container_name,
1045
+ parent_element_name=parent_group.name if parent_group is not None else None,
1035
1046
  settable=request.settable,
1036
1047
  )
1037
1048
  try:
@@ -1039,6 +1050,8 @@ class NodeManager:
1039
1050
  parameter_parent = node.get_parameter_by_name(request.parent_container_name)
1040
1051
  if parameter_parent is not None:
1041
1052
  parameter_parent.add_child(new_param)
1053
+ elif parent_group is not None:
1054
+ parent_group.add_child(new_param)
1042
1055
  else:
1043
1056
  node.add_parameter(new_param)
1044
1057
  except Exception as e:
@@ -2586,7 +2599,7 @@ class NodeManager:
2586
2599
  ) -> SerializedNodeCommands.IndirectSetParameterValueCommand | None:
2587
2600
  try:
2588
2601
  hash(value)
2589
- value_id = value
2602
+ value_id = (type(value), value)
2590
2603
  except TypeError:
2591
2604
  # Couldn't get a hash. Use the object's ID
2592
2605
  value_id = id(value)