griptape-nodes 0.55.1__py3-none-any.whl → 0.56.1__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 (60) 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/commands/init.py +88 -0
  17. griptape_nodes/cli/commands/models.py +2 -0
  18. griptape_nodes/cli/main.py +6 -1
  19. griptape_nodes/cli/shared.py +1 -0
  20. griptape_nodes/exe_types/core_types.py +130 -0
  21. griptape_nodes/exe_types/node_types.py +125 -13
  22. griptape_nodes/machines/control_flow.py +10 -0
  23. griptape_nodes/machines/dag_builder.py +21 -2
  24. griptape_nodes/machines/parallel_resolution.py +25 -10
  25. griptape_nodes/node_library/workflow_registry.py +73 -3
  26. griptape_nodes/retained_mode/events/agent_events.py +2 -0
  27. griptape_nodes/retained_mode/events/base_events.py +18 -17
  28. griptape_nodes/retained_mode/events/execution_events.py +15 -3
  29. griptape_nodes/retained_mode/events/flow_events.py +63 -7
  30. griptape_nodes/retained_mode/events/mcp_events.py +363 -0
  31. griptape_nodes/retained_mode/events/node_events.py +3 -4
  32. griptape_nodes/retained_mode/events/resource_events.py +290 -0
  33. griptape_nodes/retained_mode/events/workflow_events.py +57 -2
  34. griptape_nodes/retained_mode/griptape_nodes.py +17 -1
  35. griptape_nodes/retained_mode/managers/agent_manager.py +67 -4
  36. griptape_nodes/retained_mode/managers/event_manager.py +31 -13
  37. griptape_nodes/retained_mode/managers/flow_manager.py +731 -33
  38. griptape_nodes/retained_mode/managers/library_manager.py +15 -23
  39. griptape_nodes/retained_mode/managers/mcp_manager.py +364 -0
  40. griptape_nodes/retained_mode/managers/model_manager.py +184 -83
  41. griptape_nodes/retained_mode/managers/node_manager.py +15 -4
  42. griptape_nodes/retained_mode/managers/os_manager.py +118 -1
  43. griptape_nodes/retained_mode/managers/resource_components/__init__.py +1 -0
  44. griptape_nodes/retained_mode/managers/resource_components/capability_field.py +41 -0
  45. griptape_nodes/retained_mode/managers/resource_components/comparator.py +18 -0
  46. griptape_nodes/retained_mode/managers/resource_components/resource_instance.py +236 -0
  47. griptape_nodes/retained_mode/managers/resource_components/resource_type.py +79 -0
  48. griptape_nodes/retained_mode/managers/resource_manager.py +306 -0
  49. griptape_nodes/retained_mode/managers/resource_types/__init__.py +1 -0
  50. griptape_nodes/retained_mode/managers/resource_types/cpu_resource.py +108 -0
  51. griptape_nodes/retained_mode/managers/resource_types/os_resource.py +87 -0
  52. griptape_nodes/retained_mode/managers/settings.py +45 -0
  53. griptape_nodes/retained_mode/managers/sync_manager.py +10 -3
  54. griptape_nodes/retained_mode/managers/workflow_manager.py +447 -263
  55. griptape_nodes/traits/multi_options.py +5 -1
  56. griptape_nodes/traits/options.py +10 -2
  57. {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/METADATA +2 -2
  58. {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/RECORD +60 -37
  59. {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/WHEEL +1 -1
  60. {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,8 @@
1
1
  from dataclasses import dataclass
2
- from typing import Literal
2
+ from datetime import datetime
3
+ from typing import TYPE_CHECKING, Literal
3
4
 
4
- from griptape_nodes.node_library.workflow_registry import WorkflowMetadata
5
+ from griptape_nodes.node_library.workflow_registry import WorkflowMetadata, WorkflowShape
5
6
  from griptape_nodes.retained_mode.events.base_events import (
6
7
  RequestPayload,
7
8
  ResultPayloadFailure,
@@ -11,6 +12,9 @@ from griptape_nodes.retained_mode.events.base_events import (
11
12
  )
12
13
  from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
13
14
 
15
+ if TYPE_CHECKING:
16
+ from griptape_nodes.retained_mode.events.flow_events import SerializedFlowCommands
17
+
14
18
 
15
19
  @dataclass
16
20
  @PayloadRegistry.register
@@ -632,3 +636,54 @@ class RegisterWorkflowsFromConfigResultSuccess(WorkflowNotAlteredMixin, ResultPa
632
636
  @PayloadRegistry.register
633
637
  class RegisterWorkflowsFromConfigResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
634
638
  """Workflow registration from configuration failed. Common causes: configuration not found, invalid paths, registration errors."""
639
+
640
+
641
+ @dataclass
642
+ @PayloadRegistry.register
643
+ class SaveWorkflowFileFromSerializedFlowRequest(RequestPayload):
644
+ """Save a workflow file from serialized flow commands without registry overhead.
645
+
646
+ Use when: Creating workflow files from user-supplied subsets of existing workflows,
647
+ exporting partial workflows, creating standalone workflow files without registration.
648
+
649
+ Args:
650
+ serialized_flow_commands: The serialized commands representing the workflow structure
651
+ file_name: Name for the workflow file (without .py extension)
652
+ creation_date: Optional creation date for the workflow metadata (defaults to current time if not provided)
653
+ image_path: Optional path to workflow image/thumbnail
654
+ execution_flow_name: Optional flow name to use for execution code (defaults to file_name if not provided)
655
+ branched_from: Optional branched from information to preserve workflow lineage
656
+ workflow_shape: Optional workflow shape defining inputs and outputs for external callers
657
+ file_path: Optional specific file path to use (defaults to workspace path if not provided)
658
+
659
+ Results: SaveWorkflowFileFromSerializedFlowResultSuccess (with file path) | SaveWorkflowFileFromSerializedFlowResultFailure (save error)
660
+ """
661
+
662
+ serialized_flow_commands: "SerializedFlowCommands"
663
+ file_name: str
664
+ file_path: str | None = None
665
+ creation_date: datetime | None = None
666
+ image_path: str | None = None
667
+ execution_flow_name: str | None = None
668
+ branched_from: str | None = None
669
+ workflow_shape: WorkflowShape | None = None
670
+
671
+
672
+ @dataclass
673
+ @PayloadRegistry.register
674
+ class SaveWorkflowFileFromSerializedFlowResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
675
+ """Workflow file saved successfully from serialized flow commands.
676
+
677
+ Args:
678
+ file_path: Path where the workflow file was written
679
+ workflow_metadata: The metadata that was generated for the workflow
680
+ """
681
+
682
+ file_path: str
683
+ workflow_metadata: WorkflowMetadata
684
+
685
+
686
+ @dataclass
687
+ @PayloadRegistry.register
688
+ class SaveWorkflowFileFromSerializedFlowResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
689
+ """Workflow file save failed. Common causes: file system error, permission denied, invalid serialized commands."""
@@ -62,6 +62,7 @@ if TYPE_CHECKING:
62
62
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
63
63
  from griptape_nodes.retained_mode.managers.flow_manager import FlowManager
64
64
  from griptape_nodes.retained_mode.managers.library_manager import LibraryManager
65
+ from griptape_nodes.retained_mode.managers.mcp_manager import MCPManager
65
66
  from griptape_nodes.retained_mode.managers.model_manager import ModelManager
66
67
  from griptape_nodes.retained_mode.managers.node_manager import NodeManager
67
68
  from griptape_nodes.retained_mode.managers.object_manager import ObjectManager
@@ -69,6 +70,7 @@ if TYPE_CHECKING:
69
70
  OperationDepthManager,
70
71
  )
71
72
  from griptape_nodes.retained_mode.managers.os_manager import OSManager
73
+ from griptape_nodes.retained_mode.managers.resource_manager import ResourceManager
72
74
  from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
73
75
  from griptape_nodes.retained_mode.managers.session_manager import SessionManager
74
76
  from griptape_nodes.retained_mode.managers.static_files_manager import (
@@ -149,9 +151,11 @@ class GriptapeNodes(metaclass=SingletonMeta):
149
151
  _version_compatibility_manager: VersionCompatibilityManager
150
152
  _session_manager: SessionManager
151
153
  _engine_identity_manager: EngineIdentityManager
154
+ _mcp_manager: MCPManager
155
+ _resource_manager: ResourceManager
152
156
  _sync_manager: SyncManager
153
157
 
154
- def __init__(self) -> None:
158
+ def __init__(self) -> None: # noqa: PLR0915
155
159
  from griptape_nodes.retained_mode.managers.agent_manager import AgentManager
156
160
  from griptape_nodes.retained_mode.managers.arbitrary_code_exec_manager import (
157
161
  ArbitraryCodeExecManager,
@@ -162,6 +166,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
162
166
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
163
167
  from griptape_nodes.retained_mode.managers.flow_manager import FlowManager
164
168
  from griptape_nodes.retained_mode.managers.library_manager import LibraryManager
169
+ from griptape_nodes.retained_mode.managers.mcp_manager import MCPManager
165
170
  from griptape_nodes.retained_mode.managers.model_manager import ModelManager
166
171
  from griptape_nodes.retained_mode.managers.node_manager import NodeManager
167
172
  from griptape_nodes.retained_mode.managers.object_manager import ObjectManager
@@ -169,6 +174,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
169
174
  OperationDepthManager,
170
175
  )
171
176
  from griptape_nodes.retained_mode.managers.os_manager import OSManager
177
+ from griptape_nodes.retained_mode.managers.resource_manager import ResourceManager
172
178
  from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
173
179
  from griptape_nodes.retained_mode.managers.session_manager import SessionManager
174
180
  from griptape_nodes.retained_mode.managers.static_files_manager import (
@@ -188,6 +194,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
188
194
  # Initialize only if our managers haven't been created yet
189
195
  if not hasattr(self, "_event_manager"):
190
196
  self._event_manager = EventManager()
197
+ self._resource_manager = ResourceManager(self._event_manager)
191
198
  self._config_manager = ConfigManager(self._event_manager)
192
199
  self._os_manager = OSManager(self._event_manager)
193
200
  self._secrets_manager = SecretsManager(self._config_manager, self._event_manager)
@@ -208,6 +215,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
208
215
  self._version_compatibility_manager = VersionCompatibilityManager(self._event_manager)
209
216
  self._session_manager = SessionManager(self._event_manager)
210
217
  self._engine_identity_manager = EngineIdentityManager(self._event_manager)
218
+ self._mcp_manager = MCPManager(self._event_manager, self._config_manager)
211
219
  self._sync_manager = SyncManager(self._event_manager, self._config_manager)
212
220
 
213
221
  # Assign handlers now that these are created.
@@ -370,10 +378,18 @@ class GriptapeNodes(metaclass=SingletonMeta):
370
378
  def SessionManager(cls) -> SessionManager:
371
379
  return GriptapeNodes.get_instance()._session_manager
372
380
 
381
+ @classmethod
382
+ def MCPManager(cls) -> MCPManager:
383
+ return GriptapeNodes.get_instance()._mcp_manager
384
+
373
385
  @classmethod
374
386
  def EngineIdentityManager(cls) -> EngineIdentityManager:
375
387
  return GriptapeNodes.get_instance()._engine_identity_manager
376
388
 
389
+ @classmethod
390
+ def ResourceManager(cls) -> ResourceManager:
391
+ return GriptapeNodes.get_instance()._resource_manager
392
+
377
393
  @classmethod
378
394
  def SyncManager(cls) -> SyncManager:
379
395
  return GriptapeNodes.get_instance()._sync_manager
@@ -4,7 +4,7 @@ import logging
4
4
  import os
5
5
  import threading
6
6
  import uuid
7
- from typing import TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, ClassVar
8
8
 
9
9
  from attrs import define, field
10
10
  from griptape.artifacts import ErrorArtifact, ImageUrlArtifact, JsonArtifact
@@ -40,6 +40,10 @@ from griptape_nodes.retained_mode.events.agent_events import (
40
40
  )
41
41
  from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
42
42
  from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, ExecutionGriptapeNodeEvent, ResultPayload
43
+ from griptape_nodes.retained_mode.events.mcp_events import (
44
+ GetEnabledMCPServersRequest,
45
+ GetEnabledMCPServersResultSuccess,
46
+ )
43
47
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
44
48
  from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
45
49
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
@@ -91,6 +95,14 @@ class NodesPromptImageGenerationTool(BaseImageGenerationTool):
91
95
 
92
96
 
93
97
  class AgentManager:
98
+ # Field mappings for each transport type
99
+ TRANSPORT_FIELD_MAPPINGS: ClassVar[dict[str, list[str]]] = {
100
+ "stdio": ["command", "args", "env", "cwd", "encoding", "encoding_error_handler"],
101
+ "sse": ["url", "headers", "timeout", "sse_read_timeout"],
102
+ "streamable_http": ["url", "headers", "timeout", "sse_read_timeout", "terminate_on_close"],
103
+ "websocket": ["url"],
104
+ }
105
+
94
106
  def __init__(self, static_files_manager: StaticFilesManager, event_manager: EventManager | None = None) -> None:
95
107
  self.conversation_memory = ConversationMemory()
96
108
  self.prompt_driver = None
@@ -135,7 +147,53 @@ class AgentManager:
135
147
  "transport": "streamable_http",
136
148
  "url": f"http://localhost:{GTN_MCP_SERVER_PORT}/mcp/",
137
149
  }
138
- return MCPTool(connection=connection)
150
+ return MCPTool(connection=connection, name="mcpGriptapeNodes")
151
+
152
+ def _create_additional_mcp_tools(self, server_names: list[str]) -> list[MCPTool]:
153
+ """Create MCP tools for additional servers specified in the request."""
154
+ additional_tools = []
155
+
156
+ try:
157
+ app = GriptapeNodes()
158
+
159
+ enabled_request = GetEnabledMCPServersRequest()
160
+ enabled_result = app.handle_request(enabled_request)
161
+
162
+ if not isinstance(enabled_result, GetEnabledMCPServersResultSuccess):
163
+ msg = f"Failed to get enabled MCP servers for additional tools: {enabled_result}. Agent will continue with default MCP tool only."
164
+ logger.warning(msg)
165
+ return additional_tools
166
+
167
+ for server_name in server_names:
168
+ if server_name in enabled_result.servers:
169
+ server_config = enabled_result.servers[server_name]
170
+ connection = self._create_connection_from_mcp_config(server_config) # type: ignore[arg-type]
171
+ tool = MCPTool(connection=connection, name=f"mcp{server_name.title()}") # type: ignore[arg-type]
172
+ additional_tools.append(tool)
173
+ else:
174
+ msg = f"Additional MCP server '{server_name}' not found or not enabled"
175
+ logger.warning(msg)
176
+
177
+ except Exception as e:
178
+ msg = f"Failed to create additional MCP tools: {e}"
179
+ logger.error(msg)
180
+
181
+ return additional_tools
182
+
183
+ def _create_connection_from_mcp_config(self, server_config: dict) -> dict:
184
+ """Create connection dictionary from MCP server configuration."""
185
+ transport = server_config.get("transport", "stdio")
186
+
187
+ # Start with transport
188
+ connection = {"transport": transport}
189
+
190
+ # Map relevant fields based on transport type
191
+ fields_to_map = self.TRANSPORT_FIELD_MAPPINGS.get(transport, self.TRANSPORT_FIELD_MAPPINGS["stdio"])
192
+ for field_name in fields_to_map:
193
+ if field_name in server_config and server_config[field_name] is not None:
194
+ connection[field_name] = server_config[field_name]
195
+
196
+ return connection
139
197
 
140
198
  async def on_handle_run_agent_request(self, request: RunAgentRequest) -> ResultPayload:
141
199
  if self.prompt_driver is None:
@@ -147,7 +205,7 @@ class AgentManager:
147
205
  await asyncio.to_thread(self._on_handle_run_agent_request, request)
148
206
  return RunAgentResultStarted(result_details="Agent execution started successfully.")
149
207
 
150
- def _create_agent(self) -> Agent:
208
+ def _create_agent(self, additional_mcp_servers: list[str] | None = None) -> Agent:
151
209
  output_schema = Schema(
152
210
  {
153
211
  "generated_image_urls": [str],
@@ -161,6 +219,11 @@ class AgentManager:
161
219
  if self.mcp_tool is not None:
162
220
  tools.append(self.mcp_tool)
163
221
 
222
+ # Add additional MCP servers if specified
223
+ if additional_mcp_servers:
224
+ additional_tools = self._create_additional_mcp_tools(additional_mcp_servers)
225
+ tools.extend(additional_tools)
226
+
164
227
  return Agent(
165
228
  prompt_driver=self.prompt_driver,
166
229
  conversation_memory=self.conversation_memory,
@@ -185,7 +248,7 @@ class AgentManager:
185
248
  for url_artifact in request.url_artifacts
186
249
  if url_artifact["type"] == "ImageUrlArtifact"
187
250
  ]
188
- agent = self._create_agent()
251
+ agent = self._create_agent(additional_mcp_servers=request.additional_mcp_servers)
189
252
  *events, last_event = agent.run_stream([request.input, *artifacts])
190
253
  full_result = ""
191
254
  last_conversation_output = ""
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import inspect
5
+ import threading
5
6
  from collections import defaultdict
6
7
  from dataclasses import fields
7
8
  from typing import TYPE_CHECKING, Any, cast
@@ -43,6 +44,10 @@ class EventManager:
43
44
  self._flush_in_queue: bool = False
44
45
  # Event queue for publishing events
45
46
  self._event_queue: asyncio.Queue | None = None
47
+ # Keep track of which thread the event loop runs on
48
+ self._loop_thread_id: int | None = None
49
+ # Keep a reference to the event loop for thread-safe operations
50
+ self._event_loop: asyncio.AbstractEventLoop | None = None
46
51
 
47
52
  @property
48
53
  def event_queue(self) -> asyncio.Queue:
@@ -62,23 +67,48 @@ class EventManager:
62
67
  """
63
68
  if queue is not None:
64
69
  self._event_queue = queue
70
+ # Track which thread the event loop is running on and store loop reference
71
+ try:
72
+ self._event_loop = asyncio.get_running_loop()
73
+ self._loop_thread_id = threading.get_ident()
74
+ except RuntimeError:
75
+ self._event_loop = None
76
+ self._loop_thread_id = None
65
77
  else:
66
78
  try:
67
79
  self._event_queue = asyncio.Queue()
80
+ self._event_loop = asyncio.get_running_loop()
81
+ self._loop_thread_id = threading.get_ident()
68
82
  except RuntimeError:
69
83
  # Defer queue creation until we're in an event loop
70
84
  self._event_queue = None
85
+ self._event_loop = None
86
+ self._loop_thread_id = None
71
87
 
72
88
  def put_event(self, event: Any) -> None:
73
89
  """Put event into async queue from sync context (non-blocking).
74
90
 
91
+ Automatically detects if we're in a different thread and uses thread-safe operations.
92
+
75
93
  Args:
76
94
  event: The event to publish to the queue
77
95
  """
78
96
  if self._event_queue is None:
79
97
  return
80
98
 
81
- self._event_queue.put_nowait(event)
99
+ # Check if we need thread-safe operation
100
+ current_thread_id = threading.get_ident()
101
+
102
+ if (
103
+ self._loop_thread_id is not None
104
+ and current_thread_id != self._loop_thread_id
105
+ and self._event_loop is not None
106
+ ):
107
+ # We're in a different thread from the event loop, use thread-safe method
108
+ self._event_loop.call_soon_threadsafe(self._event_queue.put_nowait, event)
109
+ else:
110
+ # We're on the same thread as the event loop or no loop thread tracked, use direct method
111
+ self._event_queue.put_nowait(event)
82
112
 
83
113
  async def aput_event(self, event: Any) -> None:
84
114
  """Put event into async queue from async context.
@@ -91,18 +121,6 @@ class EventManager:
91
121
 
92
122
  await self._event_queue.put(event)
93
123
 
94
- def put_event_threadsafe(self, loop: Any, event: Any) -> None:
95
- """Put event into async queue from sync context in a thread-safe manner.
96
-
97
- Args:
98
- loop: The asyncio event loop to use for thread-safe operation
99
- event: The event to publish to the queue
100
- """
101
- if self._event_queue is None:
102
- return
103
-
104
- loop.call_soon_threadsafe(self._event_queue.put_nowait, event)
105
-
106
124
  def assign_manager_to_request_type(
107
125
  self,
108
126
  request_type: type[RP],