griptape-nodes 0.59.2__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 +27 -32
  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.2.dist-info → griptape_nodes-0.60.0.dist-info}/METADATA +1 -1
  39. {griptape_nodes-0.59.2.dist-info → griptape_nodes-0.60.0.dist-info}/RECORD +41 -18
  40. {griptape_nodes-0.59.2.dist-info → griptape_nodes-0.60.0.dist-info}/WHEEL +1 -1
  41. {griptape_nodes-0.59.2.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(
@@ -169,8 +169,6 @@ class LibraryManager:
169
169
  self._library_event_handler_mappings: dict[type[Payload], dict[str, LibraryManager.RegisteredEventHandler]] = {}
170
170
  # LibraryDirectory owns the FSMs and manages library lifecycle
171
171
  self._library_directory = LibraryDirectory()
172
- # Lock for synchronizing sys.path modifications during parallel library installation
173
- self._sys_path_lock = asyncio.Lock()
174
172
 
175
173
  event_manager.assign_manager_to_request_type(
176
174
  ListRegisteredLibrariesRequest, self.on_list_registered_libraries_request
@@ -726,8 +724,7 @@ class LibraryManager:
726
724
  # Get the directory containing the JSON file to resolve relative paths
727
725
  base_dir = json_path.parent.absolute()
728
726
  # Add the directory to the Python path to allow for relative imports
729
- async with self._sys_path_lock:
730
- sys.path.insert(0, str(base_dir))
727
+ sys.path.insert(0, str(base_dir))
731
728
 
732
729
  # Load the advanced library module if specified
733
730
  advanced_library_instance = None
@@ -1060,8 +1057,7 @@ class LibraryManager:
1060
1057
  )
1061
1058
  )
1062
1059
  )
1063
- async with self._sys_path_lock:
1064
- sys.path.insert(0, site_packages)
1060
+ sys.path.insert(0, site_packages)
1065
1061
 
1066
1062
  return library_venv_python_path
1067
1063
 
@@ -1503,27 +1499,6 @@ class LibraryManager:
1503
1499
 
1504
1500
  return node_class
1505
1501
 
1506
- async def _register_single_library(self, library_result: LoadLibraryMetadataFromFileResultSuccess) -> None:
1507
- """Register a single library (sandbox or config-based) and handle errors.
1508
-
1509
- Args:
1510
- library_result: The metadata result for the library to register
1511
- """
1512
- try:
1513
- if library_result.library_schema.name == LibraryManager.SANDBOX_LIBRARY_NAME:
1514
- await self._attempt_generate_sandbox_library_from_schema(
1515
- library_schema=library_result.library_schema, sandbox_directory=library_result.file_path
1516
- )
1517
- else:
1518
- register_request = RegisterLibraryFromFileRequest(
1519
- file_path=library_result.file_path, load_as_default_library=False
1520
- )
1521
- register_result = await self.register_library_from_file_request(register_request)
1522
- if isinstance(register_result, RegisterLibraryFromFileResultFailure):
1523
- logger.warning("Failed to register library from %s", library_result.file_path)
1524
- except Exception as e:
1525
- logger.warning("Failed to register library from %s with exception: %s", library_result.file_path, e)
1526
-
1527
1502
  async def load_all_libraries_from_config(self) -> None:
1528
1503
  # Load metadata for all libraries to determine which ones can be safely loaded
1529
1504
  metadata_request = LoadMetadataForAllLibrariesRequest()
@@ -1543,10 +1518,23 @@ class LibraryManager:
1543
1518
  problems=failed_library.problems,
1544
1519
  )
1545
1520
 
1546
- # Use task group for parallel library loading
1547
- async with asyncio.TaskGroup() as tg:
1548
- for library_result in metadata_result.successful_libraries:
1549
- tg.create_task(self._register_single_library(library_result))
1521
+ # Use metadata results to selectively load libraries
1522
+ for library_result in metadata_result.successful_libraries:
1523
+ if library_result.library_schema.name == LibraryManager.SANDBOX_LIBRARY_NAME:
1524
+ # Handle sandbox library - use the schema we already have
1525
+ await self._attempt_generate_sandbox_library_from_schema(
1526
+ library_schema=library_result.library_schema, sandbox_directory=library_result.file_path
1527
+ )
1528
+ else:
1529
+ # Handle config-based library - register it directly using the file path
1530
+ register_request = RegisterLibraryFromFileRequest(
1531
+ file_path=library_result.file_path, load_as_default_library=False
1532
+ )
1533
+ register_result = await self.register_library_from_file_request(register_request)
1534
+ if isinstance(register_result, RegisterLibraryFromFileResultFailure):
1535
+ # Registration failed - the failure info is already recorded in _library_file_path_to_info
1536
+ # by register_library_from_file_request, so we just log it here for visibility
1537
+ logger.warning("Failed to register library from %s", library_result.file_path)
1550
1538
 
1551
1539
  # Print 'em all pretty
1552
1540
  self.print_library_load_status()
@@ -1604,6 +1592,13 @@ class LibraryManager:
1604
1592
  session_id = GriptapeNodes.get_session_id()
1605
1593
  session_info = f" | Session: {session_id[:8]}..." if session_id else " | No Session"
1606
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
+
1607
1602
  nodes_app_url = os.getenv("GRIPTAPE_NODES_UI_BASE_URL", "https://nodes.griptape.ai")
1608
1603
  message = Panel(
1609
1604
  Align.center(
@@ -1612,7 +1607,7 @@ class LibraryManager:
1612
1607
  vertical="middle",
1613
1608
  ),
1614
1609
  title="Griptape Nodes Engine Started",
1615
- subtitle=f"[green]{engine_version}{session_info}[/green]",
1610
+ subtitle=f"[green]Version: {engine_version}{session_info}{user_info}{org_info}[/green]",
1616
1611
  border_style="green",
1617
1612
  padding=(1, 4),
1618
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)