griptape-nodes 0.41.0__py3-none-any.whl → 0.43.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 (133) hide show
  1. griptape_nodes/__init__.py +0 -0
  2. griptape_nodes/app/.python-version +0 -0
  3. griptape_nodes/app/__init__.py +1 -10
  4. griptape_nodes/app/api.py +199 -0
  5. griptape_nodes/app/app.py +140 -222
  6. griptape_nodes/app/watch.py +4 -2
  7. griptape_nodes/bootstrap/__init__.py +0 -0
  8. griptape_nodes/bootstrap/bootstrap_script.py +0 -0
  9. griptape_nodes/bootstrap/register_libraries_script.py +0 -0
  10. griptape_nodes/bootstrap/structure_config.yaml +0 -0
  11. griptape_nodes/bootstrap/workflow_executors/__init__.py +0 -0
  12. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +0 -0
  13. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +0 -0
  14. griptape_nodes/bootstrap/workflow_runners/__init__.py +0 -0
  15. griptape_nodes/bootstrap/workflow_runners/bootstrap_workflow_runner.py +0 -0
  16. griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +0 -0
  17. griptape_nodes/bootstrap/workflow_runners/subprocess_workflow_runner.py +6 -2
  18. griptape_nodes/bootstrap/workflow_runners/workflow_runner.py +0 -0
  19. griptape_nodes/drivers/__init__.py +0 -0
  20. griptape_nodes/drivers/storage/__init__.py +0 -0
  21. griptape_nodes/drivers/storage/base_storage_driver.py +0 -0
  22. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +0 -0
  23. griptape_nodes/drivers/storage/local_storage_driver.py +5 -3
  24. griptape_nodes/drivers/storage/storage_backend.py +0 -0
  25. griptape_nodes/exe_types/__init__.py +0 -0
  26. griptape_nodes/exe_types/connections.py +0 -0
  27. griptape_nodes/exe_types/core_types.py +0 -0
  28. griptape_nodes/exe_types/flow.py +68 -368
  29. griptape_nodes/exe_types/node_types.py +17 -1
  30. griptape_nodes/exe_types/type_validator.py +0 -0
  31. griptape_nodes/machines/__init__.py +0 -0
  32. griptape_nodes/machines/control_flow.py +52 -20
  33. griptape_nodes/machines/fsm.py +16 -2
  34. griptape_nodes/machines/node_resolution.py +16 -14
  35. griptape_nodes/mcp_server/__init__.py +1 -0
  36. griptape_nodes/mcp_server/server.py +126 -0
  37. griptape_nodes/mcp_server/ws_request_manager.py +268 -0
  38. griptape_nodes/node_library/__init__.py +0 -0
  39. griptape_nodes/node_library/advanced_node_library.py +0 -0
  40. griptape_nodes/node_library/library_registry.py +0 -0
  41. griptape_nodes/node_library/workflow_registry.py +2 -2
  42. griptape_nodes/py.typed +0 -0
  43. griptape_nodes/retained_mode/__init__.py +0 -0
  44. griptape_nodes/retained_mode/events/__init__.py +0 -0
  45. griptape_nodes/retained_mode/events/agent_events.py +70 -8
  46. griptape_nodes/retained_mode/events/app_events.py +137 -12
  47. griptape_nodes/retained_mode/events/arbitrary_python_events.py +23 -0
  48. griptape_nodes/retained_mode/events/base_events.py +13 -31
  49. griptape_nodes/retained_mode/events/config_events.py +87 -11
  50. griptape_nodes/retained_mode/events/connection_events.py +56 -5
  51. griptape_nodes/retained_mode/events/context_events.py +27 -4
  52. griptape_nodes/retained_mode/events/execution_events.py +99 -14
  53. griptape_nodes/retained_mode/events/flow_events.py +165 -7
  54. griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +0 -0
  55. griptape_nodes/retained_mode/events/library_events.py +195 -17
  56. griptape_nodes/retained_mode/events/logger_events.py +11 -0
  57. griptape_nodes/retained_mode/events/node_events.py +242 -22
  58. griptape_nodes/retained_mode/events/object_events.py +40 -4
  59. griptape_nodes/retained_mode/events/os_events.py +116 -3
  60. griptape_nodes/retained_mode/events/parameter_events.py +212 -8
  61. griptape_nodes/retained_mode/events/payload_registry.py +0 -0
  62. griptape_nodes/retained_mode/events/secrets_events.py +59 -7
  63. griptape_nodes/retained_mode/events/static_file_events.py +57 -4
  64. griptape_nodes/retained_mode/events/validation_events.py +39 -4
  65. griptape_nodes/retained_mode/events/workflow_events.py +188 -17
  66. griptape_nodes/retained_mode/griptape_nodes.py +89 -363
  67. griptape_nodes/retained_mode/managers/__init__.py +0 -0
  68. griptape_nodes/retained_mode/managers/agent_manager.py +49 -23
  69. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +0 -0
  70. griptape_nodes/retained_mode/managers/config_manager.py +0 -0
  71. griptape_nodes/retained_mode/managers/context_manager.py +0 -0
  72. griptape_nodes/retained_mode/managers/engine_identity_manager.py +146 -0
  73. griptape_nodes/retained_mode/managers/event_manager.py +14 -2
  74. griptape_nodes/retained_mode/managers/flow_manager.py +751 -64
  75. griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +45 -0
  76. griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +191 -0
  77. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +346 -0
  78. griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +439 -0
  79. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +17 -0
  80. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +82 -0
  81. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +116 -0
  82. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +352 -0
  83. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +104 -0
  84. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +155 -0
  85. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +18 -0
  86. griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +12 -0
  87. griptape_nodes/retained_mode/managers/library_manager.py +255 -40
  88. griptape_nodes/retained_mode/managers/node_manager.py +120 -103
  89. griptape_nodes/retained_mode/managers/object_manager.py +11 -3
  90. griptape_nodes/retained_mode/managers/operation_manager.py +0 -0
  91. griptape_nodes/retained_mode/managers/os_manager.py +582 -8
  92. griptape_nodes/retained_mode/managers/secrets_manager.py +4 -0
  93. griptape_nodes/retained_mode/managers/session_manager.py +328 -0
  94. griptape_nodes/retained_mode/managers/settings.py +7 -0
  95. griptape_nodes/retained_mode/managers/static_files_manager.py +0 -0
  96. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +2 -2
  97. griptape_nodes/retained_mode/managers/workflow_manager.py +722 -456
  98. griptape_nodes/retained_mode/retained_mode.py +44 -0
  99. griptape_nodes/retained_mode/utils/__init__.py +0 -0
  100. griptape_nodes/retained_mode/utils/engine_identity.py +141 -27
  101. griptape_nodes/retained_mode/utils/name_generator.py +0 -0
  102. griptape_nodes/traits/__init__.py +0 -0
  103. griptape_nodes/traits/add_param_button.py +0 -0
  104. griptape_nodes/traits/button.py +0 -0
  105. griptape_nodes/traits/clamp.py +0 -0
  106. griptape_nodes/traits/compare.py +0 -0
  107. griptape_nodes/traits/compare_images.py +0 -0
  108. griptape_nodes/traits/file_system_picker.py +127 -0
  109. griptape_nodes/traits/minmax.py +0 -0
  110. griptape_nodes/traits/options.py +0 -0
  111. griptape_nodes/traits/slider.py +0 -0
  112. griptape_nodes/traits/trait_registry.py +0 -0
  113. griptape_nodes/traits/traits.json +0 -0
  114. griptape_nodes/updater/__init__.py +2 -2
  115. griptape_nodes/updater/__main__.py +0 -0
  116. griptape_nodes/utils/__init__.py +0 -0
  117. griptape_nodes/utils/dict_utils.py +0 -0
  118. griptape_nodes/utils/image_preview.py +128 -0
  119. griptape_nodes/utils/metaclasses.py +0 -0
  120. griptape_nodes/version_compatibility/__init__.py +0 -0
  121. griptape_nodes/version_compatibility/versions/__init__.py +0 -0
  122. griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py +0 -0
  123. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +5 -5
  124. griptape_nodes-0.43.0.dist-info/METADATA +90 -0
  125. griptape_nodes-0.43.0.dist-info/RECORD +129 -0
  126. griptape_nodes-0.43.0.dist-info/WHEEL +4 -0
  127. {griptape_nodes-0.41.0.dist-info → griptape_nodes-0.43.0.dist-info}/entry_points.txt +1 -0
  128. griptape_nodes/app/app_sessions.py +0 -458
  129. griptape_nodes/retained_mode/utils/session_persistence.py +0 -105
  130. griptape_nodes-0.41.0.dist-info/METADATA +0 -78
  131. griptape_nodes-0.41.0.dist-info/RECORD +0 -112
  132. griptape_nodes-0.41.0.dist-info/WHEEL +0 -4
  133. griptape_nodes-0.41.0.dist-info/licenses/LICENSE +0 -201
@@ -1,4 +1,4 @@
1
- from typing import Any, Generic, TypeVar
1
+ from typing import Any, TypeVar
2
2
 
3
3
  T = TypeVar("T")
4
4
 
@@ -19,8 +19,13 @@ class State:
19
19
  """Called when exiting the state."""
20
20
  return
21
21
 
22
+ @staticmethod
23
+ def on_event(context: Any, event: Any) -> type["State"] | None: # noqa: ARG004
24
+ """Called on an event, which may trigger a State transition."""
25
+ return None
26
+
22
27
 
23
- class FSM(Generic[T]):
28
+ class FSM[T]:
24
29
  def __init__(self, context: T) -> None:
25
30
  self._context = context
26
31
  self._current_state = None
@@ -53,3 +58,12 @@ class FSM(Generic[T]):
53
58
 
54
59
  if new_state is not None:
55
60
  self.transition_state(new_state)
61
+
62
+ def handle_event(self, event: Any) -> None:
63
+ if self._current_state is None:
64
+ new_state = None
65
+ else:
66
+ new_state = self._current_state.on_event(self._context, event)
67
+
68
+ if new_state is not None:
69
+ self.transition_state(new_state)
@@ -4,7 +4,7 @@ import logging
4
4
  from collections.abc import Generator
5
5
  from concurrent.futures import Future, ThreadPoolExecutor
6
6
  from dataclasses import dataclass
7
- from typing import TYPE_CHECKING, Any
7
+ from typing import Any
8
8
 
9
9
  from griptape.events import EventBus
10
10
  from griptape.utils import with_contextvars
@@ -31,10 +31,6 @@ from griptape_nodes.retained_mode.events.parameter_events import (
31
31
  SetParameterValueRequest,
32
32
  )
33
33
 
34
- if TYPE_CHECKING:
35
- from griptape_nodes.exe_types.flow import ControlFlow
36
-
37
-
38
34
  logger = logging.getLogger("griptape_nodes")
39
35
 
40
36
 
@@ -47,12 +43,10 @@ class Focus:
47
43
 
48
44
  # This is on a per-node basis
49
45
  class ResolutionContext:
50
- flow: ControlFlow
51
46
  focus_stack: list[Focus]
52
47
  paused: bool
53
48
 
54
- def __init__(self, flow: ControlFlow) -> None:
55
- self.flow = flow
49
+ def __init__(self) -> None:
56
50
  self.focus_stack = []
57
51
  self.paused = False
58
52
 
@@ -90,7 +84,9 @@ class InitializeSpotlightState(State):
90
84
  if current_node.state == NodeResolutionState.UNRESOLVED:
91
85
  # Mark all future nodes unresolved.
92
86
  # TODO: https://github.com/griptape-ai/griptape-nodes/issues/862
93
- context.flow.connections.unresolve_future_nodes(current_node)
87
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
88
+
89
+ GriptapeNodes.FlowManager().get_connections().unresolve_future_nodes(current_node)
94
90
  current_node.initialize_spotlight()
95
91
  # Set node to resolving - we are now resolving this node.
96
92
  current_node.state = NodeResolutionState.RESOLVING
@@ -132,7 +128,9 @@ class EvaluateParameterState(State):
132
128
  def on_update(context: ResolutionContext) -> type[State] | None:
133
129
  current_node = context.focus_stack[-1].node
134
130
  current_parameter = current_node.get_current_parameter()
135
- connections = context.flow.connections
131
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
132
+
133
+ connections = GriptapeNodes.FlowManager().get_connections()
136
134
  if current_parameter is None:
137
135
  msg = "No current parameter set."
138
136
  raise ValueError(msg)
@@ -270,7 +268,10 @@ class ExecuteNodeState(State):
270
268
  )
271
269
  current_focus.process_generator = None
272
270
  current_focus.scheduled_value = None
273
- context.flow.cancel_flow_run()
271
+
272
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
273
+
274
+ GriptapeNodes.FlowManager().cancel_flow_run()
274
275
 
275
276
  EventBus.publish_event(
276
277
  ExecutionGriptapeNodeEvent(
@@ -320,7 +321,7 @@ class ExecuteNodeState(State):
320
321
  )
321
322
  )
322
323
  # Pass the value through to the new nodes.
323
- conn_output_nodes = context.flow.get_connected_output_parameters(current_node, parameter)
324
+ conn_output_nodes = GriptapeNodes.FlowManager().get_connected_output_parameters(current_node, parameter)
324
325
  for target_node, target_parameter in conn_output_nodes:
325
326
  GriptapeNodes.get_instance().handle_request(
326
327
  SetParameterValueRequest(
@@ -416,6 +417,7 @@ class ExecuteNodeState(State):
416
417
 
417
418
  # Once we've passed on the scheduled value, we should clear it out just in case
418
419
  current_focus.scheduled_value = None
420
+
419
421
  future = ExecuteNodeState.executor.submit(with_contextvars(func))
420
422
  future.add_done_callback(with_contextvars(on_future_done))
421
423
  except StopIteration:
@@ -445,8 +447,8 @@ class CompleteState(State):
445
447
  class NodeResolutionMachine(FSM[ResolutionContext]):
446
448
  """State machine for resolving node dependencies."""
447
449
 
448
- def __init__(self, flow: ControlFlow) -> None:
449
- resolution_context = ResolutionContext(flow) # Gets the flow
450
+ def __init__(self) -> None:
451
+ resolution_context = ResolutionContext()
450
452
  super().__init__(resolution_context)
451
453
 
452
454
  def resolve_node(self, node: BaseNode) -> None:
@@ -0,0 +1 @@
1
+ """Runs the Griptape Nodes MCP server."""
@@ -0,0 +1,126 @@
1
+ import contextlib
2
+ import json
3
+ import logging
4
+ import os
5
+ from collections.abc import AsyncIterator
6
+
7
+ import uvicorn
8
+ from fastapi import FastAPI
9
+ from mcp.server.lowlevel import Server
10
+ from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
11
+ from mcp.types import (
12
+ TextContent,
13
+ Tool,
14
+ )
15
+ from pydantic import TypeAdapter
16
+ from rich.logging import RichHandler
17
+ from starlette.types import Receive, Scope, Send
18
+
19
+ from griptape_nodes.mcp_server.ws_request_manager import AsyncRequestManager, WebSocketConnectionManager
20
+ from griptape_nodes.retained_mode.events.base_events import RequestPayload
21
+ from griptape_nodes.retained_mode.events.connection_events import (
22
+ CreateConnectionRequest,
23
+ DeleteConnectionRequest,
24
+ ListConnectionsForNodeRequest,
25
+ )
26
+ from griptape_nodes.retained_mode.events.flow_events import ListNodesInFlowRequest
27
+ from griptape_nodes.retained_mode.events.node_events import (
28
+ CreateNodeRequest,
29
+ DeleteNodeRequest,
30
+ GetNodeMetadataRequest,
31
+ GetNodeResolutionStateRequest,
32
+ ListParametersOnNodeRequest,
33
+ SetNodeMetadataRequest,
34
+ )
35
+ from griptape_nodes.retained_mode.events.parameter_events import (
36
+ GetParameterValueRequest,
37
+ SetParameterValueRequest,
38
+ )
39
+ from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
40
+ from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
41
+
42
+ SUPPORTED_REQUEST_EVENTS: dict[str, type[RequestPayload]] = {
43
+ # Nodes
44
+ "CreateNodeRequest": CreateNodeRequest,
45
+ "DeleteNodeRequest": DeleteNodeRequest,
46
+ "ListNodesInFlowRequest": ListNodesInFlowRequest,
47
+ "GetNodeResolutionStateRequest": GetNodeResolutionStateRequest,
48
+ "GetNodeMetadataRequest": GetNodeMetadataRequest,
49
+ "SetNodeMetadataRequest": SetNodeMetadataRequest,
50
+ # Connections
51
+ "CreateConnectionRequest": CreateConnectionRequest,
52
+ "DeleteConnectionRequest": DeleteConnectionRequest,
53
+ "ListConnectionsForNodeRequest": ListConnectionsForNodeRequest,
54
+ # Parameters
55
+ "ListParametersOnNodeRequest": ListParametersOnNodeRequest,
56
+ "GetParameterValueRequest": GetParameterValueRequest,
57
+ "SetParameterValueRequest": SetParameterValueRequest,
58
+ }
59
+
60
+ GTN_MCP_SERVER_PORT = int(os.getenv("GTN_MCP_SERVER_PORT", "9927"))
61
+
62
+ config_manager = ConfigManager()
63
+ secrets_manager = SecretsManager(config_manager)
64
+
65
+
66
+ def main(api_key: str) -> None:
67
+ """Main entry point for the Griptape Nodes MCP server."""
68
+ mcp_server_logger = logging.getLogger("griptape_nodes_mcp_server")
69
+ mcp_server_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
70
+ mcp_server_logger.setLevel(logging.INFO)
71
+ mcp_server_logger.info("Starting MCP GTN server...")
72
+
73
+ # Give these a session ID
74
+ connection_manager = WebSocketConnectionManager()
75
+ request_manager = AsyncRequestManager(connection_manager, api_key)
76
+
77
+ app = Server("mcp-gtn")
78
+
79
+ @app.list_tools()
80
+ async def list_tools() -> list[Tool]:
81
+ return [
82
+ Tool(name=event.__name__, description=event.__doc__, inputSchema=TypeAdapter(event).json_schema())
83
+ for (name, event) in SUPPORTED_REQUEST_EVENTS.items()
84
+ ]
85
+
86
+ @app.call_tool()
87
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
88
+ if name not in SUPPORTED_REQUEST_EVENTS:
89
+ msg = f"Unsupported tool: {name}"
90
+ raise ValueError(msg)
91
+
92
+ request_payload = SUPPORTED_REQUEST_EVENTS[name](**arguments)
93
+
94
+ await request_manager.connect()
95
+ result = await request_manager.create_request_event(
96
+ request_payload.__class__.__name__, request_payload.__dict__, timeout_ms=5000
97
+ )
98
+ mcp_server_logger.debug("Got result: %s", result)
99
+
100
+ return [TextContent(type="text", text=json.dumps(result))]
101
+
102
+ # Create the session manager with our app and event store
103
+ session_manager = StreamableHTTPSessionManager(
104
+ app=app,
105
+ )
106
+
107
+ @contextlib.asynccontextmanager
108
+ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
109
+ """Context manager for managing session manager lifecycle."""
110
+ async with session_manager.run():
111
+ mcp_server_logger.info("GTN MCP server started with StreamableHTTP session manager!")
112
+ try:
113
+ yield
114
+ finally:
115
+ mcp_server_logger.info("GTN MCP server shutting down...")
116
+
117
+ # Create an ASGI application using the transport
118
+ mcp_server_app = FastAPI(lifespan=lifespan)
119
+
120
+ # ASGI handler for streamable HTTP connections
121
+ async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
122
+ await session_manager.handle_request(scope, receive, send)
123
+
124
+ mcp_server_app.mount("/mcp", app=handle_streamable_http)
125
+
126
+ uvicorn.run(mcp_server_app, host="127.0.0.1", port=GTN_MCP_SERVER_PORT)
@@ -0,0 +1,268 @@
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("🔴 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("✅ 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("❌ 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
File without changes
File without changes
File without changes
@@ -13,19 +13,19 @@ from griptape_nodes.utils.metaclasses import SingletonMeta
13
13
 
14
14
 
15
15
  class WorkflowMetadata(BaseModel):
16
- LATEST_SCHEMA_VERSION: ClassVar[str] = "0.4.0"
16
+ LATEST_SCHEMA_VERSION: ClassVar[str] = "0.6.1"
17
17
 
18
18
  name: str
19
19
  schema_version: str
20
20
  engine_version_created_with: str
21
21
  node_libraries_referenced: list[LibraryNameAndVersion]
22
+ workflows_referenced: list[str] | None = None
22
23
  description: str | None = None
23
24
  image: str | None = None
24
25
  is_griptape_provided: bool | None = False
25
26
  is_template: bool | None = False
26
27
  creation_date: datetime | None = Field(default=None)
27
28
  last_modified_date: datetime | None = Field(default=None)
28
- published_workflow_id: str | None = Field(default=None)
29
29
 
30
30
 
31
31
  class WorkflowRegistry(metaclass=SingletonMeta):
griptape_nodes/py.typed CHANGED
File without changes
File without changes
File without changes