griptape-nodes 0.37.0__py3-none-any.whl → 0.38.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 (38) hide show
  1. griptape_nodes/__init__.py +292 -132
  2. griptape_nodes/app/__init__.py +1 -6
  3. griptape_nodes/app/app.py +108 -76
  4. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +80 -5
  5. griptape_nodes/drivers/storage/local_storage_driver.py +5 -1
  6. griptape_nodes/exe_types/core_types.py +84 -3
  7. griptape_nodes/exe_types/node_types.py +260 -50
  8. griptape_nodes/machines/node_resolution.py +2 -14
  9. griptape_nodes/retained_mode/events/agent_events.py +7 -0
  10. griptape_nodes/retained_mode/events/base_events.py +16 -0
  11. griptape_nodes/retained_mode/events/library_events.py +26 -0
  12. griptape_nodes/retained_mode/events/parameter_events.py +31 -0
  13. griptape_nodes/retained_mode/griptape_nodes.py +32 -0
  14. griptape_nodes/retained_mode/managers/agent_manager.py +25 -12
  15. griptape_nodes/retained_mode/managers/config_manager.py +37 -4
  16. griptape_nodes/retained_mode/managers/event_manager.py +15 -0
  17. griptape_nodes/retained_mode/managers/flow_manager.py +64 -61
  18. griptape_nodes/retained_mode/managers/library_manager.py +215 -45
  19. griptape_nodes/retained_mode/managers/node_manager.py +344 -147
  20. griptape_nodes/retained_mode/managers/operation_manager.py +6 -0
  21. griptape_nodes/retained_mode/managers/os_manager.py +6 -1
  22. griptape_nodes/retained_mode/managers/secrets_manager.py +7 -2
  23. griptape_nodes/retained_mode/managers/settings.py +2 -11
  24. griptape_nodes/retained_mode/managers/static_files_manager.py +12 -3
  25. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +105 -0
  26. griptape_nodes/retained_mode/managers/workflow_manager.py +4 -4
  27. griptape_nodes/updater/__init__.py +14 -8
  28. griptape_nodes/version_compatibility/__init__.py +1 -0
  29. griptape_nodes/version_compatibility/versions/__init__.py +1 -0
  30. griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py +1 -0
  31. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +77 -0
  32. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/METADATA +4 -1
  33. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/RECORD +36 -33
  34. griptape_nodes/app/app_websocket.py +0 -481
  35. griptape_nodes/app/nodes_api_socket_manager.py +0 -117
  36. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/WHEEL +0 -0
  37. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/entry_points.txt +0 -0
  38. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/licenses/LICENSE +0 -0
@@ -58,6 +58,9 @@ if TYPE_CHECKING:
58
58
  from griptape_nodes.retained_mode.managers.static_files_manager import (
59
59
  StaticFilesManager,
60
60
  )
61
+ from griptape_nodes.retained_mode.managers.version_compatibility_manager import (
62
+ VersionCompatibilityManager,
63
+ )
61
64
  from griptape_nodes.retained_mode.managers.workflow_manager import WorkflowManager
62
65
 
63
66
 
@@ -84,6 +87,26 @@ class Version:
84
87
  def __str__(self) -> str:
85
88
  return f"{self.major}.{self.minor}.{self.patch}"
86
89
 
90
+ def __lt__(self, other: Version) -> bool:
91
+ """Less than comparison."""
92
+ return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
93
+
94
+ def __le__(self, other: Version) -> bool:
95
+ """Less than or equal comparison."""
96
+ return (self.major, self.minor, self.patch) <= (other.major, other.minor, other.patch)
97
+
98
+ def __gt__(self, other: Version) -> bool:
99
+ """Greater than comparison."""
100
+ return (self.major, self.minor, self.patch) > (other.major, other.minor, other.patch)
101
+
102
+ def __ge__(self, other: Version) -> bool:
103
+ """Greater than or equal comparison."""
104
+ return (self.major, self.minor, self.patch) >= (other.major, other.minor, other.patch)
105
+
106
+ def __eq__(self, other: Version) -> bool: # type: ignore[override]
107
+ """Equality comparison."""
108
+ return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
109
+
87
110
 
88
111
  class GriptapeNodes(metaclass=SingletonMeta):
89
112
  _event_manager: EventManager
@@ -100,6 +123,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
100
123
  _operation_depth_manager: OperationDepthManager
101
124
  _static_files_manager: StaticFilesManager
102
125
  _agent_manager: AgentManager
126
+ _version_compatibility_manager: VersionCompatibilityManager
103
127
 
104
128
  def __init__(self) -> None:
105
129
  from griptape_nodes.retained_mode.managers.agent_manager import AgentManager
@@ -121,6 +145,9 @@ class GriptapeNodes(metaclass=SingletonMeta):
121
145
  from griptape_nodes.retained_mode.managers.static_files_manager import (
122
146
  StaticFilesManager,
123
147
  )
148
+ from griptape_nodes.retained_mode.managers.version_compatibility_manager import (
149
+ VersionCompatibilityManager,
150
+ )
124
151
  from griptape_nodes.retained_mode.managers.workflow_manager import (
125
152
  WorkflowManager,
126
153
  )
@@ -143,6 +170,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
143
170
  self._config_manager, self._secrets_manager, self._event_manager
144
171
  )
145
172
  self._agent_manager = AgentManager(self._event_manager)
173
+ self._version_compatibility_manager = VersionCompatibilityManager(self._event_manager)
146
174
 
147
175
  # Assign handlers now that these are created.
148
176
  self._event_manager.assign_manager_to_request_type(
@@ -230,6 +258,10 @@ class GriptapeNodes(metaclass=SingletonMeta):
230
258
  def AgentManager(cls) -> AgentManager:
231
259
  return GriptapeNodes.get_instance()._agent_manager
232
260
 
261
+ @classmethod
262
+ def VersionCompatibilityManager(cls) -> VersionCompatibilityManager:
263
+ return GriptapeNodes.get_instance()._version_compatibility_manager
264
+
233
265
  @classmethod
234
266
  def clear_data(cls) -> None:
235
267
  # Get canvas
@@ -1,11 +1,13 @@
1
1
  import logging
2
2
 
3
- from griptape.artifacts import ErrorArtifact
3
+ from griptape.artifacts import ErrorArtifact, TextArtifact
4
4
  from griptape.drivers.prompt.griptape_cloud import GriptapeCloudPromptDriver
5
+ from griptape.events import EventBus, FinishTaskEvent, TextChunkEvent
5
6
  from griptape.memory.structure import ConversationMemory
6
- from griptape.tasks import PromptTask
7
+ from griptape.structures import Agent
7
8
 
8
9
  from griptape_nodes.retained_mode.events.agent_events import (
10
+ AgentStreamEvent,
9
11
  ConfigureAgentRequest,
10
12
  ConfigureAgentResultFailure,
11
13
  ConfigureAgentResultSuccess,
@@ -19,7 +21,7 @@ from griptape_nodes.retained_mode.events.agent_events import (
19
21
  RunAgentResultFailure,
20
22
  RunAgentResultSuccess,
21
23
  )
22
- from griptape_nodes.retained_mode.events.base_events import ResultPayload
24
+ from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, ExecutionGriptapeNodeEvent, ResultPayload
23
25
  from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
24
26
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
25
27
  from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
@@ -53,21 +55,32 @@ class AgentManager:
53
55
  if not api_key:
54
56
  msg = f"Secret '{API_KEY_ENV_VAR}' not found"
55
57
  raise ValueError(msg)
56
- return GriptapeCloudPromptDriver(api_key=api_key)
58
+ return GriptapeCloudPromptDriver(api_key=api_key, stream=True)
57
59
 
58
60
  def on_handle_run_agent_request(self, request: RunAgentRequest) -> ResultPayload:
59
61
  try:
60
62
  if self.prompt_driver is None:
61
63
  self.prompt_driver = self._initialize_prompt_driver()
62
- task_output = PromptTask(
63
- request.input, prompt_driver=self.prompt_driver, conversation_memory=self.conversation_memory
64
- ).run()
65
- if isinstance(task_output, ErrorArtifact):
66
- details = f"Error running agent: {task_output.value}"
67
- logger.error(details)
68
- return RunAgentResultFailure(error=task_output.to_json())
69
- return RunAgentResultSuccess(output=task_output.to_json())
64
+ agent = Agent(prompt_driver=self.prompt_driver, conversation_memory=self.conversation_memory)
65
+ *events, last_event = agent.run_stream(request.input)
66
+ for event in events:
67
+ if isinstance(event, TextChunkEvent):
68
+ EventBus.publish_event(
69
+ ExecutionGriptapeNodeEvent(
70
+ wrapped_event=ExecutionEvent(payload=AgentStreamEvent(token=event.token))
71
+ )
72
+ )
73
+ if isinstance(last_event, FinishTaskEvent):
74
+ if isinstance(last_event.task_output, ErrorArtifact):
75
+ return RunAgentResultFailure(last_event.task_output.to_json())
76
+ if isinstance(last_event.task_output, TextArtifact):
77
+ return RunAgentResultSuccess(last_event.task_output.to_json())
78
+ err_msg = f"Unexpected final event: {last_event}"
79
+ logger.error(err_msg)
80
+ return RunAgentResultFailure(ErrorArtifact(last_event).to_json())
70
81
  except Exception as e:
82
+ err_msg = f"Error running agent: {e}"
83
+ logger.error(err_msg)
71
84
  return RunAgentResultFailure(ErrorArtifact(e).to_json())
72
85
 
73
86
  def on_handle_configure_agent_request(self, request: ConfigureAgentRequest) -> ResultPayload:
@@ -1,6 +1,7 @@
1
1
  import copy
2
2
  import json
3
3
  import logging
4
+ import os
4
5
  from pathlib import Path
5
6
  from typing import Any, Literal
6
7
 
@@ -40,9 +41,14 @@ USER_CONFIG_PATH = xdg_config_home() / "griptape_nodes" / "griptape_nodes_config
40
41
  class ConfigManager:
41
42
  """A class to manage application configuration and file pathing.
42
43
 
43
- This class handles loading and saving configuration from a config.json file
44
- located in the project root. If the config file is not found, it will create
45
- one with default values.
44
+ This class handles loading and saving configuration from multiple sources with the following precedence:
45
+ 1. Default configuration from Settings model (lowest priority)
46
+ 2. User global configuration from ~/.config/griptape_nodes/griptape_nodes_config.json
47
+ 3. Workspace-specific configuration from <workspace>/griptape_nodes_config.json
48
+ 4. Environment variables with GTN_CONFIG_ prefix (highest priority)
49
+
50
+ Environment variables starting with GTN_CONFIG_ are converted to config keys by removing the prefix
51
+ and converting to lowercase (e.g., GTN_CONFIG_FOO=bar becomes {"foo": "bar"}).
46
52
 
47
53
  Supports categorized configuration using dot notation (e.g., 'category.subcategory.key')
48
54
  to organize related configuration items.
@@ -51,7 +57,8 @@ class ConfigManager:
51
57
  default_config (dict): The default configuration loaded from the Settings model.
52
58
  user_config (dict): The user configuration loaded from the config file.
53
59
  workspace_config (dict): The workspace configuration loaded from the workspace config file.
54
- merged_config (dict): The merged configuration, combining default, user, and workspace settings.
60
+ env_config (dict): The configuration loaded from GTN_CONFIG_ environment variables.
61
+ merged_config (dict): The merged configuration, combining all sources in precedence order.
55
62
  """
56
63
 
57
64
  def __init__(self, event_manager: EventManager | None = None) -> None:
@@ -126,6 +133,26 @@ class ConfigManager:
126
133
 
127
134
  return [config_file for config_file in possible_config_files if config_file.exists()]
128
135
 
136
+ def _load_config_from_env_vars(self) -> dict[str, Any]:
137
+ """Load configuration values from GTN_CONFIG_ environment variables.
138
+
139
+ Environment variables starting with GTN_CONFIG_ are converted to config keys.
140
+ GTN_CONFIG_FOO=bar becomes {"foo": "bar"}
141
+ GTN_CONFIG_STORAGE_BACKEND=gtc becomes {"storage_backend": "gtc"}
142
+
143
+ Returns:
144
+ Dictionary containing config values from environment variables
145
+ """
146
+ env_config = {}
147
+ for key, value in os.environ.items():
148
+ if key.startswith("GTN_CONFIG_"):
149
+ # Remove GTN_CONFIG_ prefix and convert to lowercase
150
+ config_key = key[11:].lower() # len("GTN_CONFIG_") = 11
151
+ env_config[config_key] = value
152
+ logger.debug("Loaded config from env var: %s -> %s", key, config_key)
153
+
154
+ return env_config
155
+
129
156
  def load_configs(self) -> None:
130
157
  """Load configs from the user config file and the workspace config file.
131
158
 
@@ -159,6 +186,12 @@ class ConfigManager:
159
186
  self.workspace_config = {}
160
187
  logger.debug("Workspace config file not found")
161
188
 
189
+ # Merge in configuration from GTN_CONFIG_ environment variables (highest priority)
190
+ self.env_config = self._load_config_from_env_vars()
191
+ if self.env_config:
192
+ merged_config = merge_dicts(merged_config, self.env_config)
193
+ logger.debug("Merged config from environment variables: %s", list(self.env_config.keys()))
194
+
162
195
  # Validate the full config against the Settings model.
163
196
  try:
164
197
  Settings.model_validate(merged_config)
@@ -8,11 +8,14 @@ from typing_extensions import TypeVar
8
8
 
9
9
  from griptape_nodes.retained_mode.events.base_events import (
10
10
  AppPayload,
11
+ EventRequest,
11
12
  EventResultFailure,
12
13
  EventResultSuccess,
14
+ FlushParameterChangesRequest,
13
15
  GriptapeNodeEvent,
14
16
  RequestPayload,
15
17
  ResultPayload,
18
+ WorkflowAlteredMixin,
16
19
  )
17
20
 
18
21
  if TYPE_CHECKING:
@@ -30,6 +33,11 @@ class EventManager:
30
33
  # Dictionary to store ALL SUBSCRIBERS to app events.
31
34
  self._app_event_listeners: dict[type[AppPayload], set[Callable]] = {}
32
35
  self.current_active_node: str | None = None
36
+ # Boolean that lets us know if there is currently a FlushParameterChangesRequest in the event queue.
37
+ self._flush_in_queue: bool = False
38
+
39
+ def clear_flush_in_queue(self) -> None:
40
+ self._flush_in_queue = False
33
41
 
34
42
  def assign_manager_to_request_type(
35
43
  self,
@@ -96,6 +104,13 @@ class EventManager:
96
104
  result=result_payload,
97
105
  retained_mode=retained_mode_str,
98
106
  )
107
+ # If the result is a success, and the WorkflowAlteredMixin is present, that means the flow has been changed in some way.
108
+ # In that case, we need to flush the element changes, so we add one to the event queue.
109
+ if isinstance(result_event.result, WorkflowAlteredMixin) and not self._flush_in_queue:
110
+ from griptape_nodes.app.app import event_queue
111
+
112
+ event_queue.put(EventRequest(request=FlushParameterChangesRequest()))
113
+ self._flush_in_queue = True
99
114
  else:
100
115
  result_event = EventResultFailure(
101
116
  request=request,
@@ -3,15 +3,16 @@ from __future__ import annotations
3
3
  import logging
4
4
  from typing import TYPE_CHECKING, cast
5
5
 
6
- from griptape.events import EventBus
7
-
8
6
  from griptape_nodes.exe_types.core_types import (
9
7
  ParameterContainer,
10
8
  ParameterMode,
11
9
  )
12
10
  from griptape_nodes.exe_types.flow import ControlFlow
13
11
  from griptape_nodes.exe_types.node_types import BaseNode, NodeResolutionState
14
- from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, ExecutionGriptapeNodeEvent
12
+ from griptape_nodes.retained_mode.events.base_events import (
13
+ FlushParameterChangesRequest,
14
+ FlushParameterChangesResultSuccess,
15
+ )
15
16
  from griptape_nodes.retained_mode.events.connection_events import (
16
17
  CreateConnectionRequest,
17
18
  CreateConnectionResultFailure,
@@ -81,7 +82,6 @@ from griptape_nodes.retained_mode.events.node_events import (
81
82
  SerializeNodeToCommandsResultSuccess,
82
83
  )
83
84
  from griptape_nodes.retained_mode.events.parameter_events import (
84
- AlterElementEvent,
85
85
  SetParameterValueRequest,
86
86
  )
87
87
  from griptape_nodes.retained_mode.events.validation_events import (
@@ -130,6 +130,7 @@ class FlowManager:
130
130
  event_manager.assign_manager_to_request_type(
131
131
  DeserializeFlowFromCommandsRequest, self.on_deserialize_flow_from_commands
132
132
  )
133
+ event_manager.assign_manager_to_request_type(FlushParameterChangesRequest, self.on_flush_request)
133
134
 
134
135
  self._name_to_parent_name = {}
135
136
 
@@ -607,37 +608,32 @@ class FlowManager:
607
608
  return CreateConnectionResultFailure()
608
609
 
609
610
  # Let the source make any internal handling decisions now that the Connection has been made.
610
- modified_source_parameters = set()
611
- source_node.after_outgoing_connection(
612
- source_parameter=source_param,
613
- target_node=target_node,
614
- target_parameter=target_param,
615
- modified_parameters_set=modified_source_parameters,
616
- )
611
+ try:
612
+ source_node.after_outgoing_connection(
613
+ source_parameter=source_param, target_node=target_node, target_parameter=target_param
614
+ )
615
+ except TypeError:
616
+ source_node.after_outgoing_connection(
617
+ source_parameter=source_param,
618
+ target_node=target_node,
619
+ target_parameter=target_param,
620
+ modified_parameters_set=set(),
621
+ )
617
622
 
618
623
  # And target.
619
- modified_target_parameters = set()
620
- target_node.after_incoming_connection(
621
- source_node=source_node,
622
- source_parameter=source_param,
623
- target_parameter=target_param,
624
- modified_parameters_set=modified_target_parameters,
625
- )
626
-
627
- if modified_source_parameters:
628
- for modified_parameter_name in modified_source_parameters:
629
- # TODO: https://github.com/griptape-ai/griptape-nodes/issues/865
630
- modified_parameter = source_node.get_parameter_by_name(modified_parameter_name)
631
- if modified_parameter is not None:
632
- modified_request = AlterElementEvent(element_details=modified_parameter.to_event(source_node))
633
- EventBus.publish_event(ExecutionGriptapeNodeEvent(ExecutionEvent(payload=modified_request)))
634
- if modified_target_parameters:
635
- for modified_parameter_name in modified_target_parameters:
636
- # TODO: https://github.com/griptape-ai/griptape-nodes/issues/865
637
- modified_parameter = target_node.get_parameter_by_name(modified_parameter_name)
638
- if modified_parameter is not None:
639
- modified_request = AlterElementEvent(element_details=modified_parameter.to_event(target_node))
640
- EventBus.publish_event(ExecutionGriptapeNodeEvent(ExecutionEvent(payload=modified_request)))
624
+ try:
625
+ target_node.after_incoming_connection(
626
+ source_node=source_node,
627
+ source_parameter=source_param,
628
+ target_parameter=target_param,
629
+ )
630
+ except TypeError:
631
+ target_node.after_incoming_connection(
632
+ source_node=source_node,
633
+ source_parameter=source_param,
634
+ target_parameter=target_param,
635
+ modified_parameters_set=set(),
636
+ )
641
637
 
642
638
  details = f'Connected "{source_node_name}.{request.source_parameter_name}" to "{target_node_name}.{request.target_parameter_name}"'
643
639
  logger.debug(details)
@@ -801,37 +797,33 @@ class FlowManager:
801
797
  )
802
798
  except KeyError as e:
803
799
  logger.warning(e)
804
- modified_source_parameters = set()
805
800
  # Let the source make any internal handling decisions now that the Connection has been REMOVED.
806
- source_node.after_outgoing_connection_removed(
807
- source_parameter=source_param,
808
- target_node=target_node,
809
- target_parameter=target_param,
810
- modified_parameters_set=modified_source_parameters,
811
- )
801
+ try:
802
+ source_node.after_outgoing_connection_removed(
803
+ source_parameter=source_param, target_node=target_node, target_parameter=target_param
804
+ )
805
+ except TypeError:
806
+ source_node.after_outgoing_connection_removed(
807
+ source_parameter=source_param,
808
+ target_node=target_node,
809
+ target_parameter=target_param,
810
+ modified_parameters_set=set(),
811
+ )
812
812
 
813
813
  # And target.
814
- modified_target_parameters = set()
815
- target_node.after_incoming_connection_removed(
816
- source_node=source_node,
817
- source_parameter=source_param,
818
- target_parameter=target_param,
819
- modified_parameters_set=modified_target_parameters,
820
- )
821
- if modified_source_parameters:
822
- for modified_parameter_name in modified_source_parameters:
823
- # TODO: https://github.com/griptape-ai/griptape-nodes/issues/865
824
- modified_parameter = source_node.get_parameter_by_name(modified_parameter_name)
825
- if modified_parameter is not None:
826
- modified_request = AlterElementEvent(element_details=modified_parameter.to_event(source_node))
827
- EventBus.publish_event(ExecutionGriptapeNodeEvent(ExecutionEvent(payload=modified_request)))
828
- if modified_target_parameters:
829
- for modified_parameter_name in modified_target_parameters:
830
- # TODO: https://github.com/griptape-ai/griptape-nodes/issues/865
831
- modified_parameter = target_node.get_parameter_by_name(modified_parameter_name)
832
- if modified_parameter is not None:
833
- modified_request = AlterElementEvent(element_details=modified_parameter.to_event(target_node))
834
- EventBus.publish_event(ExecutionGriptapeNodeEvent(ExecutionEvent(payload=modified_request)))
814
+ try:
815
+ target_node.after_incoming_connection_removed(
816
+ source_node=source_node,
817
+ source_parameter=source_param,
818
+ target_parameter=target_param,
819
+ )
820
+ except TypeError:
821
+ target_node.after_incoming_connection_removed(
822
+ source_node=source_node,
823
+ source_parameter=source_param,
824
+ target_parameter=target_param,
825
+ modified_parameters_set=set(),
826
+ )
835
827
 
836
828
  details = f'Connection "{source_node_name}.{request.source_parameter_name}" to "{target_node_name}.{request.target_parameter_name}" deleted.'
837
829
  logger.debug(details)
@@ -1387,3 +1379,14 @@ class FlowManager:
1387
1379
  details = f"Successfully deserialized Flow '{flow_name}'."
1388
1380
  logger.debug(details)
1389
1381
  return DeserializeFlowFromCommandsResultSuccess(flow_name=flow_name)
1382
+
1383
+ def on_flush_request(self, request: FlushParameterChangesRequest) -> ResultPayload: # noqa: ARG002
1384
+ obj_manager = GriptapeNodes.ObjectManager()
1385
+ GriptapeNodes.EventManager().clear_flush_in_queue()
1386
+ # Get all flows and their nodes
1387
+ nodes = obj_manager.get_filtered_subset(type=BaseNode)
1388
+ for node in nodes.values():
1389
+ # Only flush if there are actually tracked parameters
1390
+ if node._tracked_parameters:
1391
+ node.emit_parameter_changes()
1392
+ return FlushParameterChangesResultSuccess()