griptape-nodes 0.64.10__py3-none-any.whl → 0.65.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 (55) hide show
  1. griptape_nodes/app/app.py +25 -5
  2. griptape_nodes/cli/commands/init.py +65 -54
  3. griptape_nodes/cli/commands/libraries.py +92 -85
  4. griptape_nodes/cli/commands/self.py +121 -0
  5. griptape_nodes/common/node_executor.py +2142 -101
  6. griptape_nodes/exe_types/base_iterative_nodes.py +1004 -0
  7. griptape_nodes/exe_types/connections.py +114 -19
  8. griptape_nodes/exe_types/core_types.py +225 -7
  9. griptape_nodes/exe_types/flow.py +3 -3
  10. griptape_nodes/exe_types/node_types.py +681 -225
  11. griptape_nodes/exe_types/param_components/README.md +414 -0
  12. griptape_nodes/exe_types/param_components/api_key_provider_parameter.py +200 -0
  13. griptape_nodes/exe_types/param_components/huggingface/huggingface_model_parameter.py +2 -0
  14. griptape_nodes/exe_types/param_components/huggingface/huggingface_repo_file_parameter.py +79 -5
  15. griptape_nodes/exe_types/param_types/parameter_button.py +443 -0
  16. griptape_nodes/machines/control_flow.py +77 -38
  17. griptape_nodes/machines/dag_builder.py +148 -70
  18. griptape_nodes/machines/parallel_resolution.py +61 -35
  19. griptape_nodes/machines/sequential_resolution.py +11 -113
  20. griptape_nodes/retained_mode/events/app_events.py +1 -0
  21. griptape_nodes/retained_mode/events/base_events.py +16 -13
  22. griptape_nodes/retained_mode/events/connection_events.py +3 -0
  23. griptape_nodes/retained_mode/events/execution_events.py +35 -0
  24. griptape_nodes/retained_mode/events/flow_events.py +15 -2
  25. griptape_nodes/retained_mode/events/library_events.py +347 -0
  26. griptape_nodes/retained_mode/events/node_events.py +48 -0
  27. griptape_nodes/retained_mode/events/os_events.py +86 -3
  28. griptape_nodes/retained_mode/events/project_events.py +15 -1
  29. griptape_nodes/retained_mode/events/workflow_events.py +48 -1
  30. griptape_nodes/retained_mode/griptape_nodes.py +6 -2
  31. griptape_nodes/retained_mode/managers/config_manager.py +10 -8
  32. griptape_nodes/retained_mode/managers/event_manager.py +168 -0
  33. griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +2 -0
  34. griptape_nodes/retained_mode/managers/fitness_problems/libraries/old_xdg_location_warning_problem.py +43 -0
  35. griptape_nodes/retained_mode/managers/flow_manager.py +664 -123
  36. griptape_nodes/retained_mode/managers/library_manager.py +1143 -139
  37. griptape_nodes/retained_mode/managers/model_manager.py +2 -3
  38. griptape_nodes/retained_mode/managers/node_manager.py +148 -25
  39. griptape_nodes/retained_mode/managers/object_manager.py +3 -1
  40. griptape_nodes/retained_mode/managers/operation_manager.py +3 -1
  41. griptape_nodes/retained_mode/managers/os_manager.py +1158 -122
  42. griptape_nodes/retained_mode/managers/secrets_manager.py +2 -3
  43. griptape_nodes/retained_mode/managers/settings.py +21 -1
  44. griptape_nodes/retained_mode/managers/sync_manager.py +2 -3
  45. griptape_nodes/retained_mode/managers/workflow_manager.py +358 -104
  46. griptape_nodes/retained_mode/retained_mode.py +3 -3
  47. griptape_nodes/traits/button.py +44 -2
  48. griptape_nodes/traits/file_system_picker.py +2 -2
  49. griptape_nodes/utils/file_utils.py +101 -0
  50. griptape_nodes/utils/git_utils.py +1226 -0
  51. griptape_nodes/utils/library_utils.py +122 -0
  52. {griptape_nodes-0.64.10.dist-info → griptape_nodes-0.65.0.dist-info}/METADATA +2 -1
  53. {griptape_nodes-0.64.10.dist-info → griptape_nodes-0.65.0.dist-info}/RECORD +55 -47
  54. {griptape_nodes-0.64.10.dist-info → griptape_nodes-0.65.0.dist-info}/WHEEL +1 -1
  55. {griptape_nodes-0.64.10.dist-info → griptape_nodes-0.65.0.dist-info}/entry_points.txt +0 -0
@@ -188,7 +188,9 @@ class GriptapeNodes(metaclass=SingletonMeta):
188
188
 
189
189
  try:
190
190
  result_event = event_mgr.handle_request(request=request)
191
- event_mgr.put_event(GriptapeNodeEvent(wrapped_event=result_event))
191
+ # Only queue result event if not suppressed
192
+ if not event_mgr.should_suppress_event(result_event):
193
+ event_mgr.put_event(GriptapeNodeEvent(wrapped_event=result_event))
192
194
  except Exception as e:
193
195
  logger.exception(
194
196
  "Unhandled exception while processing request of type %s. "
@@ -214,7 +216,9 @@ class GriptapeNodes(metaclass=SingletonMeta):
214
216
 
215
217
  try:
216
218
  result_event = await event_mgr.ahandle_request(request=request)
217
- await event_mgr.aput_event(GriptapeNodeEvent(wrapped_event=result_event))
219
+ # Only queue result event if not suppressed
220
+ if not event_mgr.should_suppress_event(result_event):
221
+ await event_mgr.aput_event(GriptapeNodeEvent(wrapped_event=result_event))
218
222
  except Exception as e:
219
223
  logger.exception(
220
224
  "Unhandled exception while processing async request of type %s. "
@@ -33,7 +33,7 @@ from griptape_nodes.retained_mode.events.config_events import (
33
33
  SetConfigValueResultSuccess,
34
34
  )
35
35
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
36
- from griptape_nodes.retained_mode.managers.settings import Settings
36
+ from griptape_nodes.retained_mode.managers.settings import WORKFLOWS_TO_REGISTER_KEY, Settings
37
37
  from griptape_nodes.utils.dict_utils import get_dot_value, merge_dicts, set_dot_value
38
38
 
39
39
  logger = logging.getLogger("griptape_nodes")
@@ -210,7 +210,7 @@ class ConfigManager:
210
210
  An exception is made for `workflows_to_register` since resetting it gives the appearance of the user losing their workflows.
211
211
  """
212
212
  # TODO: https://github.com/griptape-ai/griptape-nodes/issues/1241 need a better way to annotate fields to ignore.
213
- workflows_to_register = self.get_config_value("app_events.on_app_initialization_complete.workflows_to_register")
213
+ workflows_to_register = self.get_config_value(WORKFLOWS_TO_REGISTER_KEY)
214
214
  USER_CONFIG_PATH.write_text(
215
215
  json.dumps(
216
216
  {
@@ -226,7 +226,7 @@ class ConfigManager:
226
226
  self.load_configs()
227
227
 
228
228
  def save_user_workflow_json(self, workflow_file_name: str) -> None:
229
- config_loc = "app_events.on_app_initialization_complete.workflows_to_register"
229
+ config_loc = WORKFLOWS_TO_REGISTER_KEY
230
230
  existing_workflows = self.get_config_value(config_loc)
231
231
  if not existing_workflows:
232
232
  existing_workflows = []
@@ -234,14 +234,14 @@ class ConfigManager:
234
234
  self.set_config_value(config_loc, existing_workflows)
235
235
 
236
236
  def delete_user_workflow(self, workflow_file_name: str) -> None:
237
- default_workflows = self.get_config_value("app_events.on_app_initialization_complete.workflows_to_register")
237
+ default_workflows = self.get_config_value(WORKFLOWS_TO_REGISTER_KEY)
238
238
  if default_workflows:
239
239
  default_workflows = [
240
240
  saved_workflow
241
241
  for saved_workflow in default_workflows
242
242
  if (saved_workflow.lower() != workflow_file_name.lower())
243
243
  ]
244
- self.set_config_value("app_events.on_app_initialization_complete.workflows_to_register", default_workflows)
244
+ self.set_config_value(WORKFLOWS_TO_REGISTER_KEY, default_workflows)
245
245
 
246
246
  def get_full_path(self, relative_path: str) -> Path:
247
247
  """Get a full path by combining the base path with a relative path.
@@ -288,7 +288,7 @@ class ConfigManager:
288
288
 
289
289
  if value is None:
290
290
  msg = f"Config key '{key}' not found in config file."
291
- logger.warning(msg)
291
+ logger.debug(msg)
292
292
  return None
293
293
 
294
294
  if should_load_env_var_if_detected and isinstance(value, str) and value.startswith("$"):
@@ -528,7 +528,9 @@ class ConfigManager:
528
528
  level: The log level to set (e.g., 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL').
529
529
  """
530
530
  try:
531
- logger.setLevel(level.upper())
532
- except ValueError:
531
+ level_upper = level.upper()
532
+ log_level = getattr(logging, level_upper)
533
+ logger.setLevel(log_level)
534
+ except (ValueError, AttributeError):
533
535
  logger.error("Invalid log level %s. Defaulting to INFO.", level)
534
536
  logger.setLevel(logging.INFO)
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import inspect
5
+ import logging
5
6
  import threading
6
7
  from collections import defaultdict
7
8
  from dataclasses import fields
@@ -13,14 +14,18 @@ from typing_extensions import TypedDict, TypeVar
13
14
  from griptape_nodes.exe_types.node_types import BaseNode
14
15
  from griptape_nodes.retained_mode.events.base_events import (
15
16
  AppPayload,
17
+ BaseEvent,
16
18
  EventResultFailure,
17
19
  EventResultSuccess,
20
+ ProgressEvent,
18
21
  RequestPayload,
22
+ ResultDetails,
19
23
  ResultPayload,
20
24
  )
21
25
  from griptape_nodes.utils.async_utils import call_function
22
26
 
23
27
  if TYPE_CHECKING:
28
+ import types
24
29
  from collections.abc import Awaitable, Callable
25
30
 
26
31
 
@@ -51,6 +56,8 @@ class EventManager:
51
56
  self._loop_thread_id: int | None = None
52
57
  # Keep a reference to the event loop for thread-safe operations
53
58
  self._event_loop: asyncio.AbstractEventLoop | None = None
59
+ # Per-event reference counting for event suppression
60
+ self._event_suppression_counts: dict[type, int] = {}
54
61
 
55
62
  @property
56
63
  def event_queue(self) -> asyncio.Queue:
@@ -59,6 +66,15 @@ class EventManager:
59
66
  raise ValueError(msg)
60
67
  return self._event_queue
61
68
 
69
+ def should_suppress_event(self, event: BaseEvent | ProgressEvent) -> bool:
70
+ """Check if events should be suppressed from being sent to websockets."""
71
+ event_type = type(event)
72
+ return self._event_suppression_counts.get(event_type, 0) > 0
73
+
74
+ def clear_event_suppression(self) -> None:
75
+ """Clear all event suppression counts."""
76
+ self._event_suppression_counts.clear()
77
+
62
78
  def initialize_queue(self, queue: asyncio.Queue | None = None) -> None:
63
79
  """Set the event queue for this manager.
64
80
 
@@ -162,6 +178,28 @@ class EventManager:
162
178
  if request_type in self._request_type_to_manager:
163
179
  del self._request_type_to_manager[request_type]
164
180
 
181
+ def _override_result_log_level(self, result: ResultPayload, level: int) -> None:
182
+ """Override the log level on all result details.
183
+
184
+ Args:
185
+ result: The result payload to modify
186
+ level: The new log level to set
187
+ """
188
+ if isinstance(result.result_details, ResultDetails):
189
+ for detail in result.result_details.result_details:
190
+ detail.level = level
191
+
192
+ def _log_result_details(self, result: ResultPayload) -> None:
193
+ """Log the result details at their specified levels.
194
+
195
+ Args:
196
+ result: The result payload containing details to log
197
+ """
198
+ if isinstance(result.result_details, ResultDetails):
199
+ logger = logging.getLogger("griptape_nodes")
200
+ for detail in result.result_details.result_details:
201
+ logger.log(detail.level, detail.message)
202
+
165
203
  def _handle_request_core(
166
204
  self,
167
205
  request: RP,
@@ -182,6 +220,13 @@ class EventManager:
182
220
  if workflow_mgr.should_squelch_workflow_altered():
183
221
  callback_result.altered_workflow_state = False
184
222
 
223
+ # Override failure log level if requested
224
+ if callback_result.failed() and request.failure_log_level is not None:
225
+ self._override_result_log_level(callback_result, request.failure_log_level)
226
+
227
+ # Log result details (after potential level override)
228
+ self._log_result_details(callback_result)
229
+
185
230
  retained_mode_str = None
186
231
  # If request_id exists, that means it's a direct request from the GUI (not internal), and should be echoed by retained mode.
187
232
  if depth_manager.is_top_level() and context.get("request_id") is not None:
@@ -332,3 +377,126 @@ class EventManager:
332
377
  # Only flush if there are actually tracked parameters
333
378
  if node._tracked_parameters:
334
379
  node.emit_parameter_changes()
380
+
381
+
382
+ class EventSuppressionContext:
383
+ """Context manager to suppress events from being sent to websockets.
384
+
385
+ Use this to prevent internal operations (like deserialization/deletion of iteration flows)
386
+ from sending events to the GUI while still allowing the operations to complete normally.
387
+
388
+ Uses per-event reference counting to track nested suppression contexts.
389
+ Each event type maintains its own reference count, and is only unsuppressed
390
+ when its count reaches zero.
391
+ """
392
+
393
+ events_to_suppress: set[type]
394
+
395
+ def __init__(self, manager: EventManager, events_to_suppress: set[type]):
396
+ self.manager = manager
397
+ self.events_to_suppress = events_to_suppress
398
+
399
+ def __enter__(self) -> None:
400
+ for event_type in self.events_to_suppress:
401
+ current_count = self.manager._event_suppression_counts.get(event_type, 0)
402
+ self.manager._event_suppression_counts[event_type] = current_count + 1
403
+
404
+ def __exit__(
405
+ self,
406
+ exc_type: type[BaseException] | None,
407
+ exc_value: BaseException | None,
408
+ exc_traceback: types.TracebackType | None,
409
+ ) -> None:
410
+ for event_type in self.events_to_suppress:
411
+ current_count = self.manager._event_suppression_counts.get(event_type, 0)
412
+ if current_count <= 1:
413
+ self.manager._event_suppression_counts.pop(event_type, None)
414
+ else:
415
+ self.manager._event_suppression_counts[event_type] = current_count - 1
416
+
417
+
418
+ class EventTranslationContext:
419
+ """Context manager to translate node names in events from packaged to original names.
420
+
421
+ Use this to make loop execution events reference the original nodes that the user placed,
422
+ rather than the packaged node copies. This allows the UI to highlight the correct nodes
423
+ during loop execution.
424
+ """
425
+
426
+ def __init__(self, manager: EventManager, node_name_mapping: dict[str, str]):
427
+ """Initialize the event translation context.
428
+
429
+ Args:
430
+ manager: The EventManager to intercept events from
431
+ node_name_mapping: Dict mapping packaged node names to original node names
432
+ """
433
+ self.manager = manager
434
+ self.node_name_mapping = node_name_mapping
435
+ self.original_put_event: Any = None
436
+
437
+ def __enter__(self) -> None:
438
+ """Enter the context and start translating events."""
439
+ self.original_put_event = self.manager.put_event
440
+ self.manager.put_event = self._translate_and_put # type: ignore[method-assign]
441
+
442
+ def __exit__(
443
+ self,
444
+ exc_type: type[BaseException] | None,
445
+ exc_value: BaseException | None,
446
+ exc_traceback: types.TracebackType | None,
447
+ ) -> None:
448
+ """Exit the context and restore original event sending."""
449
+ self.manager.put_event = self.original_put_event # type: ignore[method-assign]
450
+
451
+ def _translate_and_put(self, event: Any) -> None:
452
+ """Translate node names in events and put them in the queue.
453
+
454
+ Args:
455
+ event: The event to potentially translate and send
456
+ """
457
+ # Check if event has node_name attribute and needs translation
458
+ if hasattr(event, "node_name"):
459
+ node_name = event.node_name
460
+ if node_name in self.node_name_mapping:
461
+ # Create a copy of the event with the translated node name
462
+ translated_event = self._copy_event_with_translated_name(event)
463
+ self.original_put_event(translated_event)
464
+ return
465
+
466
+ # No translation needed, send as-is
467
+ self.original_put_event(event)
468
+
469
+ def _copy_event_with_translated_name(self, event: Any) -> Any:
470
+ """Create a copy of an event with the node name translated to the original name.
471
+
472
+ Args:
473
+ event: The event to copy and translate
474
+
475
+ Returns:
476
+ A new event instance with the translated node name
477
+ """
478
+ # Get the original node name from the mapping
479
+ node_name = event.node_name
480
+ original_node_name = self.node_name_mapping[node_name]
481
+
482
+ # Get the event class
483
+ event_class = type(event)
484
+
485
+ # Create a dict of all event attributes
486
+ if hasattr(event, "model_dump"):
487
+ event_dict = event.model_dump()
488
+ elif hasattr(event, "__dict__"):
489
+ event_dict = event.__dict__.copy()
490
+ else:
491
+ # Can't copy this event, return as-is
492
+ return event
493
+
494
+ # Replace the node name with the original name
495
+ event_dict["node_name"] = original_node_name
496
+
497
+ # Create a new event instance with the translated name
498
+ try:
499
+ return event_class(**event_dict)
500
+ except Exception:
501
+ # If we can't create a new instance, return the original
502
+ return event
@@ -22,6 +22,7 @@ from .modified_parameters_set_removed_problem import ModifiedParametersSetRemove
22
22
  from .node_class_not_base_node_problem import NodeClassNotBaseNodeProblem
23
23
  from .node_class_not_found_problem import NodeClassNotFoundProblem
24
24
  from .node_module_import_problem import NodeModuleImportProblem
25
+ from .old_xdg_location_warning_problem import OldXdgLocationWarningProblem
25
26
  from .sandbox_directory_missing_problem import SandboxDirectoryMissingProblem
26
27
  from .ui_options_field_modified_incompatible_problem import UiOptionsFieldModifiedIncompatibleProblem
27
28
  from .ui_options_field_modified_warning_problem import UiOptionsFieldModifiedWarningProblem
@@ -51,6 +52,7 @@ __all__ = [
51
52
  "NodeClassNotBaseNodeProblem",
52
53
  "NodeClassNotFoundProblem",
53
54
  "NodeModuleImportProblem",
55
+ "OldXdgLocationWarningProblem",
54
56
  "SandboxDirectoryMissingProblem",
55
57
  "UiOptionsFieldModifiedIncompatibleProblem",
56
58
  "UiOptionsFieldModifiedWarningProblem",
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+
6
+ from xdg_base_dirs import xdg_data_home
7
+
8
+ from griptape_nodes.retained_mode.managers.fitness_problems.libraries.library_problem import LibraryProblem
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @dataclass
14
+ class OldXdgLocationWarningProblem(LibraryProblem):
15
+ """Problem warning that a library is located in the old XDG data directory.
16
+
17
+ This is a warning-level problem (FLAWED status) - the library will still
18
+ load and function normally, but users should migrate to the new location.
19
+ """
20
+
21
+ old_path: str
22
+
23
+ @classmethod
24
+ def collate_problems_for_display(cls, instances: list[OldXdgLocationWarningProblem]) -> str:
25
+ """Display old XDG location warning.
26
+
27
+ There should only be one instance per library since each LibraryInfo
28
+ is already associated with a specific library path.
29
+ """
30
+ if len(instances) > 1:
31
+ logger.error(
32
+ "OldXdgLocationWarningProblem: Expected 1 instance but got %s. Each LibraryInfo should only have one OldXdgLocationWarningProblem.",
33
+ len(instances),
34
+ )
35
+
36
+ old_libraries_path = xdg_data_home() / "griptape_nodes" / "libraries"
37
+ return (
38
+ f"WARNING: Starting with version 0.65.0, libraries are now managed in your workspace directory. "
39
+ f"This library is located in {old_libraries_path} and will not receive updates because it is not tracked "
40
+ f"by the library manager. "
41
+ f"To migrate: run 'gtn init'. "
42
+ f"The library will continue to function normally until migrated."
43
+ )