griptape-nodes 0.57.1__py3-none-any.whl → 0.58.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 (51) hide show
  1. griptape_nodes/api_client/__init__.py +9 -0
  2. griptape_nodes/api_client/client.py +279 -0
  3. griptape_nodes/api_client/request_client.py +273 -0
  4. griptape_nodes/app/app.py +57 -150
  5. griptape_nodes/bootstrap/utils/python_subprocess_executor.py +1 -1
  6. griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +22 -50
  7. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +6 -1
  8. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +27 -46
  9. griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +7 -0
  10. griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +3 -1
  11. griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +3 -1
  12. griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +16 -1
  13. griptape_nodes/common/node_executor.py +466 -0
  14. griptape_nodes/drivers/storage/base_storage_driver.py +0 -11
  15. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +7 -25
  16. griptape_nodes/drivers/storage/local_storage_driver.py +2 -2
  17. griptape_nodes/exe_types/connections.py +37 -9
  18. griptape_nodes/exe_types/core_types.py +1 -1
  19. griptape_nodes/exe_types/node_types.py +115 -22
  20. griptape_nodes/machines/control_flow.py +48 -7
  21. griptape_nodes/machines/parallel_resolution.py +98 -29
  22. griptape_nodes/machines/sequential_resolution.py +61 -22
  23. griptape_nodes/node_library/library_registry.py +24 -1
  24. griptape_nodes/node_library/workflow_registry.py +38 -2
  25. griptape_nodes/retained_mode/events/execution_events.py +8 -1
  26. griptape_nodes/retained_mode/events/flow_events.py +90 -3
  27. griptape_nodes/retained_mode/events/node_events.py +17 -10
  28. griptape_nodes/retained_mode/events/workflow_events.py +5 -0
  29. griptape_nodes/retained_mode/griptape_nodes.py +16 -219
  30. griptape_nodes/retained_mode/managers/config_manager.py +0 -46
  31. griptape_nodes/retained_mode/managers/engine_identity_manager.py +225 -74
  32. griptape_nodes/retained_mode/managers/flow_manager.py +1276 -230
  33. griptape_nodes/retained_mode/managers/library_manager.py +7 -8
  34. griptape_nodes/retained_mode/managers/node_manager.py +197 -9
  35. griptape_nodes/retained_mode/managers/secrets_manager.py +26 -0
  36. griptape_nodes/retained_mode/managers/session_manager.py +264 -227
  37. griptape_nodes/retained_mode/managers/settings.py +4 -38
  38. griptape_nodes/retained_mode/managers/static_files_manager.py +3 -3
  39. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +135 -6
  40. griptape_nodes/retained_mode/managers/workflow_manager.py +206 -78
  41. griptape_nodes/servers/mcp.py +23 -15
  42. griptape_nodes/utils/async_utils.py +36 -0
  43. griptape_nodes/utils/dict_utils.py +8 -2
  44. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +11 -6
  45. griptape_nodes/version_compatibility/workflow_versions/v0_7_0/local_executor_argument_addition.py +12 -5
  46. {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.0.dist-info}/METADATA +4 -3
  47. {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.0.dist-info}/RECORD +49 -47
  48. {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.0.dist-info}/WHEEL +1 -1
  49. griptape_nodes/retained_mode/utils/engine_identity.py +0 -245
  50. griptape_nodes/servers/ws_request_manager.py +0 -268
  51. {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.0.dist-info}/entry_points.txt +0 -0
@@ -1,268 +0,0 @@
1
- import asyncio
2
- import contextlib
3
- import json
4
- import logging
5
- import os
6
- import uuid
7
- from collections.abc import Callable
8
- from typing import Any, Generic, TypeVar
9
- from urllib.parse import urljoin
10
-
11
- import websockets
12
-
13
- logger = logging.getLogger("griptape_nodes_mcp_server")
14
-
15
- T = TypeVar("T")
16
-
17
-
18
- class WebSocketConnectionManager:
19
- """Python equivalent of WebSocketConnectionManager in TypeScript."""
20
-
21
- def __init__(
22
- self,
23
- websocket_url: str = urljoin(
24
- os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai").replace("http", "ws"),
25
- "/ws/engines/events?version=v2",
26
- ),
27
- ):
28
- self.websocket_url = websocket_url
29
- self.websocket: Any = None
30
- self.connected = False
31
- self.event_handlers: dict[str, list[Callable]] = {}
32
- self.request_handlers: dict[str, tuple[Callable, Callable]] = {}
33
- self._process_task: asyncio.Task | None = None
34
-
35
- async def send(self, data: dict[str, Any]) -> None:
36
- """Send a message to the WebSocket server."""
37
- if not self.websocket:
38
- msg = "Not connected to WebSocket server"
39
- raise ConnectionError(msg)
40
-
41
- try:
42
- message = json.dumps(data)
43
- await self.websocket.send(message)
44
- logger.debug("Sent message: %s", message)
45
- except Exception as e:
46
- logger.error("Failed to send message: %s", e)
47
- raise
48
-
49
- async def _process_messages(self) -> None:
50
- """Process incoming WebSocket messages."""
51
- if not self.websocket:
52
- logger.warning("WebSocket is not connected, cannot process messages")
53
- return
54
-
55
- try:
56
- async for message in self.websocket:
57
- try:
58
- data = json.loads(message)
59
- logger.debug("📥 Received message: %s", message)
60
- await self._handle_message(data)
61
- except json.JSONDecodeError:
62
- logger.error("Failed to parse message: %s", message)
63
- except Exception as e:
64
- logger.error("Error processing message: %s", e)
65
- except websockets.exceptions.ConnectionClosed:
66
- logger.warning("WebSocket connection closed")
67
- self.connected = False
68
- except asyncio.CancelledError:
69
- # Task was cancelled, just exit
70
- pass
71
- except Exception as e:
72
- logger.error("Error in message processing loop: %s", e)
73
- self.connected = False
74
-
75
- async def _handle_message(self, data: dict[str, Any]) -> None:
76
- request = data.get("payload", {}).get("request", {})
77
- request_id = request.get("request_id")
78
-
79
- if request_id and request_id in self.request_handlers:
80
- success_handler, failure_handler = self.request_handlers[request_id]
81
- try:
82
- if data.get("type") == "success_result":
83
- success_handler(data, request)
84
- else:
85
- failure_handler(data, request)
86
- except Exception as e:
87
- logger.error("Error in request handler: %s", e)
88
-
89
- def subscribe_to_request_event(
90
- self, success_handler: Callable[[Any, Any], None], failure_handler: Callable[[Any, Any], None]
91
- ) -> str:
92
- """Subscribe to a request-response event."""
93
- request_id = str(uuid.uuid4())
94
- self.request_handlers[request_id] = (success_handler, failure_handler)
95
- return request_id
96
-
97
- def unsubscribe_from_request_event(self, request_id: str) -> None:
98
- """Unsubscribe from a request-response event."""
99
- if request_id in self.request_handlers:
100
- del self.request_handlers[request_id]
101
-
102
-
103
- class AsyncRequestManager(Generic[T]): # noqa: UP046
104
- def __init__(self, connection_manager: WebSocketConnectionManager, api_key: str):
105
- self.connection_manager = connection_manager
106
- self.api_key = api_key
107
-
108
- async def _subscribe_to_topic(self, topic: str) -> None:
109
- """Subscribe to a specific topic in the message bus."""
110
- if self.connection_manager.websocket is None:
111
- logger.warning("WebSocket connection not available for subscribing to topic")
112
- return
113
-
114
- try:
115
- body = {"type": "subscribe", "topic": topic, "payload": {}}
116
- await self.connection_manager.websocket.send(json.dumps(body))
117
- logger.debug("Subscribed to topic: %s", topic)
118
- except websockets.exceptions.WebSocketException as e:
119
- logger.error("Error subscribing to topic %s: %s", topic, e)
120
- except Exception as e:
121
- logger.error("Unexpected error while subscribing to topic %s: %s", topic, e)
122
-
123
- async def _subscribe_to_topics(self) -> None:
124
- from griptape_nodes.retained_mode.managers.session_manager import SessionManager
125
- from griptape_nodes.retained_mode.utils.engine_identity import EngineIdentity
126
-
127
- # Subscribe to response topic (engine discovery)
128
- await self._subscribe_to_topic("response")
129
-
130
- # Get engine ID and subscribe to engine_id/response
131
- engine_id = EngineIdentity.get_engine_id()
132
- if engine_id:
133
- await self._subscribe_to_topic(f"engines/{engine_id}/response")
134
- else:
135
- logger.warning("Engine ID not available for subscription")
136
-
137
- # Get session ID and subscribe to session_id/response if available
138
- session_id = SessionManager.get_saved_session_id()
139
- if session_id:
140
- topic = f"sessions/{session_id}/response"
141
- await self._subscribe_to_topic(topic)
142
- else:
143
- logger.info("No session ID available for subscription")
144
-
145
- async def connect(self) -> None:
146
- """Connect to the WebSocket server."""
147
- from griptape_nodes.app.app import _create_websocket_connection
148
-
149
- try:
150
- self.connection_manager.websocket = await _create_websocket_connection(self.api_key)
151
- logger.debug("🟢 WebSocket connection established: %s", self.connection_manager.websocket)
152
-
153
- await self._subscribe_to_topics()
154
-
155
- # Start processing messages
156
- self.connection_manager._process_task = asyncio.create_task(self.connection_manager._process_messages())
157
-
158
- except Exception as e:
159
- self.connection_manager.connected = False
160
- logger.error("[red]X[/red] WebSocket connection failed: %s", str(e))
161
- msg = f"Failed to connect to WebSocket: {e!s}"
162
- raise ConnectionError(msg) from e
163
-
164
- async def disconnect(self) -> None:
165
- """Disconnect from the WebSocket server."""
166
- if self.connection_manager.websocket:
167
- await self.connection_manager.websocket.close()
168
- self.connection_manager.websocket = None
169
- self.connection_manager.connected = False
170
-
171
- # Cancel processing task if it's running
172
- if self.connection_manager._process_task:
173
- self.connection_manager._process_task.cancel()
174
- with contextlib.suppress(asyncio.CancelledError):
175
- await self.connection_manager._process_task
176
- self.connection_manager._process_task = None
177
-
178
- logger.debug("WebSocket disconnected")
179
-
180
- async def send_api_message(self, data: dict[str, Any]) -> None:
181
- """Send a message to the API via WebSocket."""
182
- try:
183
- await self.connection_manager.send(data)
184
- except ConnectionError as e:
185
- logger.error("Failed to send API message: %s", e)
186
- raise
187
- except Exception as e:
188
- logger.error("Unexpected error sending API message: %s", e)
189
- raise
190
-
191
- async def create_event(self, request_type: str, payload: dict[str, Any]) -> None:
192
- """Send an event to the API without waiting for a response."""
193
- from griptape_nodes.app.app import determine_request_topic
194
-
195
- logger.debug("Creating Event: %s - %s", request_type, json.dumps(payload))
196
-
197
- data = {"event_type": "EventRequest", "request_type": request_type, "request": payload}
198
- topic = determine_request_topic()
199
-
200
- request_data = {"payload": data, "type": data["event_type"], "topic": topic}
201
-
202
- if not request_data["payload"]["request"].get("request_id"):
203
- request_data["payload"]["request"]["request_id"] = ""
204
-
205
- await self.send_api_message(request_data)
206
-
207
- async def create_request_event(
208
- self, request_type: str, payload: dict[str, Any], timeout_ms: int | None = None
209
- ) -> T:
210
- """Send a request and wait for its response.
211
-
212
- Args:
213
- request_type: Type of request to send
214
- payload: Data to send with the request
215
- timeout_ms: Optional timeout in milliseconds
216
-
217
- Returns:
218
- The response data
219
-
220
- Raises:
221
- asyncio.TimeoutError: If the request times out
222
- Exception: If the request fails
223
- """
224
- # Create a future that will be resolved when the response arrives
225
- response_future = asyncio.Future()
226
-
227
- # Convert timeout from milliseconds to seconds for asyncio
228
- timeout_sec = timeout_ms / 1000 if timeout_ms else None
229
-
230
- # Define handlers that will resolve/reject the future
231
- def success_handler(response: Any, _: Any) -> None:
232
- if not response_future.done():
233
- result = response.get("payload", {}).get("result", "Success")
234
- logger.debug("[green]OK[/green] Request succeeded: %s", result)
235
- response_future.set_result(result)
236
-
237
- def failure_handler(response: Any, _: Any) -> None:
238
- if not response_future.done():
239
- error = (
240
- response.get("payload", {}).get("result", {}).get("exception", "Unknown error") or "Unknown error"
241
- )
242
- logger.error("[red]X[/red] Request failed: %s", error)
243
- response_future.set_exception(Exception(error))
244
-
245
- # Generate request ID and subscribe
246
- request_id = self.connection_manager.subscribe_to_request_event(success_handler, failure_handler)
247
- payload["request_id"] = request_id
248
-
249
- logger.debug("Request (%s): %s %s", request_id, request_type, json.dumps(payload))
250
-
251
- try:
252
- # Send the event
253
- await self.create_event(request_type, payload)
254
-
255
- # Wait for the response with optional timeout
256
- if timeout_sec:
257
- return await asyncio.wait_for(response_future, timeout=timeout_sec)
258
- return await response_future
259
-
260
- except TimeoutError:
261
- logger.error("Request timed out after %s ms: %s", timeout_ms, request_id)
262
- self.connection_manager.unsubscribe_from_request_event(request_id)
263
- raise
264
-
265
- except Exception as e:
266
- logger.error("Request failed: %s - %s", request_id, e)
267
- self.connection_manager.unsubscribe_from_request_event(request_id)
268
- raise