griptape-nodes 0.42.0__py3-none-any.whl → 0.43.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 (132) hide show
  1. griptape_nodes/__init__.py +0 -0
  2. griptape_nodes/app/.python-version +0 -0
  3. griptape_nodes/app/__init__.py +1 -6
  4. griptape_nodes/app/api.py +199 -0
  5. griptape_nodes/app/app.py +140 -225
  6. griptape_nodes/app/watch.py +1 -1
  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 +2 -1
  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 +0 -0
  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 +41 -12
  33. griptape_nodes/machines/fsm.py +16 -2
  34. griptape_nodes/machines/node_resolution.py +0 -0
  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 +1 -1
  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 +0 -0
  46. griptape_nodes/retained_mode/events/app_events.py +6 -2
  47. griptape_nodes/retained_mode/events/arbitrary_python_events.py +0 -0
  48. griptape_nodes/retained_mode/events/base_events.py +6 -6
  49. griptape_nodes/retained_mode/events/config_events.py +0 -0
  50. griptape_nodes/retained_mode/events/connection_events.py +0 -0
  51. griptape_nodes/retained_mode/events/context_events.py +0 -0
  52. griptape_nodes/retained_mode/events/execution_events.py +0 -0
  53. griptape_nodes/retained_mode/events/flow_events.py +0 -0
  54. griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +0 -0
  55. griptape_nodes/retained_mode/events/library_events.py +2 -2
  56. griptape_nodes/retained_mode/events/logger_events.py +0 -0
  57. griptape_nodes/retained_mode/events/node_events.py +0 -0
  58. griptape_nodes/retained_mode/events/object_events.py +0 -0
  59. griptape_nodes/retained_mode/events/os_events.py +104 -2
  60. griptape_nodes/retained_mode/events/parameter_events.py +0 -0
  61. griptape_nodes/retained_mode/events/payload_registry.py +0 -0
  62. griptape_nodes/retained_mode/events/secrets_events.py +0 -0
  63. griptape_nodes/retained_mode/events/static_file_events.py +0 -0
  64. griptape_nodes/retained_mode/events/validation_events.py +0 -0
  65. griptape_nodes/retained_mode/events/workflow_events.py +0 -0
  66. griptape_nodes/retained_mode/griptape_nodes.py +43 -40
  67. griptape_nodes/retained_mode/managers/__init__.py +0 -0
  68. griptape_nodes/retained_mode/managers/agent_manager.py +48 -22
  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 +0 -0
  73. griptape_nodes/retained_mode/managers/event_manager.py +0 -0
  74. griptape_nodes/retained_mode/managers/flow_manager.py +2 -0
  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 +144 -39
  88. griptape_nodes/retained_mode/managers/node_manager.py +86 -72
  89. griptape_nodes/retained_mode/managers/object_manager.py +0 -0
  90. griptape_nodes/retained_mode/managers/operation_manager.py +0 -0
  91. griptape_nodes/retained_mode/managers/os_manager.py +517 -12
  92. griptape_nodes/retained_mode/managers/secrets_manager.py +0 -0
  93. griptape_nodes/retained_mode/managers/session_manager.py +0 -0
  94. griptape_nodes/retained_mode/managers/settings.py +0 -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 +199 -2
  98. griptape_nodes/retained_mode/retained_mode.py +0 -0
  99. griptape_nodes/retained_mode/utils/__init__.py +0 -0
  100. griptape_nodes/retained_mode/utils/engine_identity.py +0 -0
  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.1.dist-info/METADATA +90 -0
  125. griptape_nodes-0.43.1.dist-info/RECORD +129 -0
  126. griptape_nodes-0.43.1.dist-info/WHEEL +4 -0
  127. {griptape_nodes-0.42.0.dist-info → griptape_nodes-0.43.1.dist-info}/entry_points.txt +1 -0
  128. griptape_nodes/app/app_sessions.py +0 -554
  129. griptape_nodes-0.42.0.dist-info/METADATA +0 -78
  130. griptape_nodes-0.42.0.dist-info/RECORD +0 -113
  131. griptape_nodes-0.42.0.dist-info/WHEEL +0 -4
  132. griptape_nodes-0.42.0.dist-info/licenses/LICENSE +0 -201
@@ -1,554 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- import binascii
5
- import json
6
- import logging
7
- import os
8
- import signal
9
- import sys
10
- import threading
11
- from pathlib import Path
12
- from queue import Queue
13
- from typing import Any, cast
14
- from urllib.parse import urljoin
15
-
16
- import uvicorn
17
- from fastapi import FastAPI, HTTPException, Request
18
- from fastapi.middleware.cors import CORSMiddleware
19
- from fastapi.staticfiles import StaticFiles
20
- from griptape.events import (
21
- EventBus,
22
- EventListener,
23
- )
24
- from rich.align import Align
25
- from rich.console import Console
26
- from rich.logging import RichHandler
27
- from rich.panel import Panel
28
- from websockets.asyncio.client import connect
29
- from websockets.exceptions import ConnectionClosed, WebSocketException
30
-
31
- # This import is necessary to register all events, even if not technically used
32
- from griptape_nodes.retained_mode.events import app_events, execution_events
33
- from griptape_nodes.retained_mode.events.app_events import AppEndSessionResultSuccess, AppStartSessionResultSuccess
34
- from griptape_nodes.retained_mode.events.base_events import (
35
- AppEvent,
36
- EventRequest,
37
- EventResultFailure,
38
- EventResultSuccess,
39
- ExecutionEvent,
40
- ExecutionGriptapeNodeEvent,
41
- GriptapeNodeEvent,
42
- ProgressEvent,
43
- deserialize_event,
44
- )
45
- from griptape_nodes.retained_mode.events.logger_events import LogHandlerEvent
46
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
47
-
48
- # This is a global event queue that will be used to pass events between threads
49
- event_queue = Queue()
50
-
51
- # Global WebSocket connection for sending events
52
- ws_connection_for_sending = None
53
- event_loop = None
54
-
55
- # Whether to enable the static server
56
- STATIC_SERVER_ENABLED = os.getenv("STATIC_SERVER_ENABLED", "true").lower() == "true"
57
- # Host of the static server
58
- STATIC_SERVER_HOST = os.getenv("STATIC_SERVER_HOST", "localhost")
59
- # Port of the static server
60
- STATIC_SERVER_PORT = int(os.getenv("STATIC_SERVER_PORT", "8124"))
61
- # URL path for the static server
62
- STATIC_SERVER_URL = os.getenv("STATIC_SERVER_URL", "/static")
63
- # Log level for the static server
64
- STATIC_SERVER_LOG_LEVEL = os.getenv("STATIC_SERVER_LOG_LEVEL", "info").lower()
65
-
66
-
67
- class EventLogHandler(logging.Handler):
68
- """Custom logging handler that emits log messages as AppEvents.
69
-
70
- This is used to forward log messages to the event queue so they can be sent to the GUI.
71
- """
72
-
73
- def emit(self, record: logging.LogRecord) -> None:
74
- event_queue.put(
75
- AppEvent(
76
- payload=LogHandlerEvent(message=record.getMessage(), levelname=record.levelname, created=record.created)
77
- )
78
- )
79
-
80
-
81
- # Logger for this module. Important that this is not the same as the griptape_nodes logger or else we'll have infinite log events.
82
- logger = logging.getLogger("griptape_nodes_app")
83
- console = Console()
84
-
85
-
86
- def start_app() -> None:
87
- """Main entry point for the Griptape Nodes app.
88
-
89
- Starts the event loop and listens for events from the Nodes API.
90
- """
91
- _init_event_listeners()
92
-
93
- griptape_nodes_logger = logging.getLogger("griptape_nodes")
94
- # When running as an app, we want to forward all log messages to the event queue so they can be sent to the GUI
95
- griptape_nodes_logger.addHandler(EventLogHandler())
96
- griptape_nodes_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
97
- griptape_nodes_logger.setLevel(logging.INFO)
98
-
99
- # Listen for any signals to exit the app
100
- for sig in (signal.SIGINT, signal.SIGTERM):
101
- signal.signal(sig, lambda *_: sys.exit(0))
102
-
103
- # SSE subscription pushes events into event_queue
104
- threading.Thread(target=_listen_for_api_events, daemon=True).start()
105
-
106
- if STATIC_SERVER_ENABLED:
107
- threading.Thread(target=_serve_static_server, daemon=True).start()
108
-
109
- _process_event_queue()
110
-
111
-
112
- def _serve_static_server() -> None:
113
- """Run FastAPI with Uvicorn in order to serve static files produced by nodes."""
114
- config_manager = GriptapeNodes.ConfigManager()
115
- app = FastAPI()
116
-
117
- static_dir = config_manager.workspace_path / config_manager.merged_config["static_files_directory"]
118
-
119
- if not static_dir.exists():
120
- static_dir.mkdir(parents=True, exist_ok=True)
121
-
122
- app.add_middleware(
123
- CORSMiddleware,
124
- allow_origins=[
125
- os.getenv("GRIPTAPE_NODES_UI_BASE_URL", "https://app.nodes.griptape.ai"),
126
- "https://app.nodes-staging.griptape.ai",
127
- "http://localhost:5173",
128
- ],
129
- allow_credentials=True,
130
- allow_methods=["OPTIONS", "GET", "POST", "PUT"],
131
- allow_headers=["*"],
132
- )
133
-
134
- app.mount(
135
- STATIC_SERVER_URL,
136
- StaticFiles(directory=static_dir),
137
- name="static",
138
- )
139
-
140
- @app.post("/static-upload-urls")
141
- async def create_static_file_upload_url(request: Request) -> dict:
142
- """Create a URL for uploading a static file.
143
-
144
- Similar to a presigned URL, but for uploading files to the static server.
145
- """
146
- base_url = request.base_url
147
- body = await request.json()
148
- file_name = body["file_name"]
149
- url = urljoin(str(base_url), f"/static-uploads/{file_name}")
150
-
151
- return {"url": url}
152
-
153
- @app.put("/static-uploads/{file_path:path}")
154
- async def create_static_file(request: Request, file_path: str) -> dict:
155
- """Upload a static file to the static server."""
156
- if not STATIC_SERVER_ENABLED:
157
- msg = "Static server is not enabled. Please set STATIC_SERVER_ENABLED to True."
158
- raise ValueError(msg)
159
-
160
- file_full_path = Path(static_dir / file_path)
161
-
162
- # Create parent directories if they don't exist
163
- file_full_path.parent.mkdir(parents=True, exist_ok=True)
164
-
165
- data = await request.body()
166
- try:
167
- file_full_path.write_bytes(data)
168
- except binascii.Error as e:
169
- msg = f"Invalid base64 encoding for file {file_path}."
170
- logger.error(msg)
171
- raise HTTPException(status_code=400, detail=msg) from e
172
- except (OSError, PermissionError) as e:
173
- msg = f"Failed to write file {file_path} to {config_manager.workspace_path}: {e}"
174
- logger.error(msg)
175
- raise HTTPException(status_code=500, detail=msg) from e
176
-
177
- static_url = f"http://{STATIC_SERVER_HOST}:{STATIC_SERVER_PORT}{STATIC_SERVER_URL}/{file_path}"
178
- return {"url": static_url}
179
-
180
- @app.post("/engines/request")
181
- async def create_event(request: Request) -> None:
182
- body = await request.json()
183
- __process_api_event(body)
184
-
185
- logging.getLogger("uvicorn").addHandler(
186
- RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True)
187
- )
188
-
189
- uvicorn.run(
190
- app, host=STATIC_SERVER_HOST, port=STATIC_SERVER_PORT, log_level=STATIC_SERVER_LOG_LEVEL, log_config=None
191
- )
192
-
193
-
194
- def _init_event_listeners() -> None:
195
- """Set up the Griptape EventBus EventListeners."""
196
- EventBus.add_event_listener(
197
- event_listener=EventListener(on_event=__process_node_event, event_types=[GriptapeNodeEvent])
198
- )
199
-
200
- EventBus.add_event_listener(
201
- event_listener=EventListener(
202
- on_event=__process_execution_node_event,
203
- event_types=[ExecutionGriptapeNodeEvent],
204
- )
205
- )
206
-
207
- EventBus.add_event_listener(
208
- event_listener=EventListener(
209
- on_event=__process_progress_event,
210
- event_types=[ProgressEvent],
211
- )
212
- )
213
-
214
- EventBus.add_event_listener(
215
- event_listener=EventListener(
216
- on_event=__process_app_event, # pyright: ignore[reportArgumentType] TODO: https://github.com/griptape-ai/griptape-nodes/issues/868
217
- event_types=[AppEvent], # pyright: ignore[reportArgumentType] TODO: https://github.com/griptape-ai/griptape-nodes/issues/868
218
- )
219
- )
220
-
221
-
222
- async def _alisten_for_api_requests() -> None:
223
- """Listen for events from the Nodes API and process them asynchronously."""
224
- global ws_connection_for_sending, event_loop # noqa: PLW0603
225
- event_loop = asyncio.get_running_loop() # Store the event loop reference
226
- nodes_app_url = os.getenv("GRIPTAPE_NODES_UI_BASE_URL", "https://nodes.griptape.ai")
227
- logger.info("Listening for events from Nodes API via async WebSocket")
228
-
229
- # Auto reconnect https://websockets.readthedocs.io/en/stable/reference/asyncio/client.html#opening-a-connection
230
- connection_stream = __create_async_websocket_connection()
231
- initialized = False
232
- async for ws_connection in connection_stream:
233
- try:
234
- ws_connection_for_sending = ws_connection # Store for sending events
235
-
236
- if not initialized:
237
- __broadcast_app_initialization_complete(nodes_app_url)
238
- initialized = True
239
-
240
- # Subscribe to engine ID and session ID on every new connection
241
- await __subscribe_to_engine_and_session(ws_connection)
242
-
243
- async for message in ws_connection:
244
- try:
245
- data = json.loads(message)
246
-
247
- __process_api_event(data)
248
- except Exception:
249
- logger.exception("Error processing event, skipping.")
250
- except ConnectionClosed:
251
- continue
252
- except Exception as e:
253
- logger.error("Error while listening for events. Retrying in 2 seconds... %s", e)
254
- await asyncio.sleep(2)
255
-
256
-
257
- def _listen_for_api_events() -> None:
258
- """Run the async WebSocket listener in an event loop."""
259
- asyncio.run(_alisten_for_api_requests())
260
-
261
-
262
- def __process_node_event(event: GriptapeNodeEvent) -> None:
263
- """Process GriptapeNodeEvents and send them to the API."""
264
- # Emit the result back to the GUI
265
- result_event = event.wrapped_event
266
- if isinstance(result_event, EventResultSuccess):
267
- dest_socket = "success_result"
268
- # Handle session start events specially
269
- __handle_session_events(result_event)
270
- elif isinstance(result_event, EventResultFailure):
271
- dest_socket = "failure_result"
272
- else:
273
- msg = f"Unknown/unsupported result event type encountered: '{type(result_event)}'."
274
- raise TypeError(msg) from None
275
-
276
- # Don't send events over the wire that don't have a request_id set (e.g. engine-internal events)
277
- __schedule_async_task(__emit_message(dest_socket, result_event.json(), topic=result_event.response_topic))
278
-
279
-
280
- def __handle_session_events(result_event: EventResultSuccess) -> None:
281
- """Handle session start/end events by managing subscriptions."""
282
- global ws_connection_for_sending # noqa: PLW0602
283
-
284
- if isinstance(result_event.result, AppStartSessionResultSuccess):
285
- # Subscribe to the new session topic
286
- session_id = result_event.result.session_id
287
- if session_id and ws_connection_for_sending:
288
- topic = f"sessions/{session_id}/request"
289
- __schedule_async_task(__subscribe_to_topic(ws_connection_for_sending, topic))
290
- logger.info("Subscribed to new session topic: %s", topic)
291
-
292
- elif isinstance(result_event.result, AppEndSessionResultSuccess):
293
- # Unsubscribe from the ended session topic
294
- session_id = result_event.result.session_id
295
- if session_id and ws_connection_for_sending:
296
- topic = f"sessions/{session_id}/request"
297
- __schedule_async_task(__unsubscribe_from_topic(ws_connection_for_sending, topic))
298
- logger.info("Unsubscribed from ended session topic: %s", topic)
299
-
300
-
301
- def __process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
302
- """Process ExecutionGriptapeNodeEvents and send them to the API."""
303
- result_event = event.wrapped_event
304
- if type(result_event.payload).__name__ == "NodeStartProcessEvent":
305
- GriptapeNodes.EventManager().current_active_node = result_event.payload.node_name
306
-
307
- if type(result_event.payload).__name__ == "ResumeNodeProcessingEvent":
308
- node_name = result_event.payload.node_name
309
- logger.info("Resuming Node '%s'", node_name)
310
- flow_name = GriptapeNodes.NodeManager().get_node_parent_flow_by_name(node_name)
311
- request = EventRequest(request=execution_events.SingleExecutionStepRequest(flow_name=flow_name))
312
- event_queue.put(request)
313
-
314
- if type(result_event.payload).__name__ == "NodeFinishProcessEvent":
315
- if result_event.payload.node_name != GriptapeNodes.EventManager().current_active_node:
316
- msg = "Node start and finish do not match."
317
- raise KeyError(msg) from None
318
- GriptapeNodes.EventManager().current_active_node = None
319
- __schedule_async_task(__emit_message("execution_event", result_event.json()))
320
-
321
-
322
- def __process_progress_event(gt_event: ProgressEvent) -> None:
323
- """Process Griptape framework events and send them to the API."""
324
- node_name = gt_event.node_name
325
- if node_name:
326
- value = gt_event.value
327
- payload = execution_events.GriptapeEvent(
328
- node_name=node_name, parameter_name=gt_event.parameter_name, type=type(gt_event).__name__, value=value
329
- )
330
- event_to_emit = ExecutionEvent(payload=payload)
331
- __schedule_async_task(__emit_message("execution_event", event_to_emit.json()))
332
-
333
-
334
- def __process_app_event(event: AppEvent) -> None:
335
- """Process AppEvents and send them to the API."""
336
- # Let Griptape Nodes broadcast it.
337
- GriptapeNodes.broadcast_app_event(event.payload)
338
-
339
- __schedule_async_task(__emit_message("app_event", event.json()))
340
-
341
-
342
- def _process_event_queue() -> None:
343
- """Listen for events in the event queue and process them.
344
-
345
- Event queue will be populated by background threads listening for events from the Nodes API.
346
- """
347
- while True:
348
- event = event_queue.get(block=True)
349
- if isinstance(event, EventRequest):
350
- request_payload = event.request
351
- GriptapeNodes.handle_request(
352
- request_payload, response_topic=event.response_topic, request_id=event.request_id
353
- )
354
- elif isinstance(event, AppEvent):
355
- __process_app_event(event)
356
- else:
357
- logger.warning("Unknown event type encountered: '%s'.", type(event))
358
-
359
- event_queue.task_done()
360
-
361
-
362
- def __create_async_websocket_connection() -> Any:
363
- """Create an async WebSocket connection to the Nodes API."""
364
- secrets_manager = GriptapeNodes.SecretsManager()
365
- api_key = secrets_manager.get_secret("GT_CLOUD_API_KEY")
366
- if api_key is None:
367
- message = Panel(
368
- Align.center(
369
- "[bold red]Nodes API key is not set, please run [code]gtn init[/code] with a valid key: [/bold red]"
370
- "[code]gtn init --api-key <your key>[/code]\n"
371
- "[bold red]You can generate a new key from [/bold red][bold blue][link=https://nodes.griptape.ai]https://nodes.griptape.ai[/link][/bold blue]",
372
- ),
373
- title="🔑 ❌ Missing Nodes API Key",
374
- border_style="red",
375
- padding=(1, 4),
376
- )
377
- console.print(message)
378
- sys.exit(1)
379
-
380
- endpoint = urljoin(
381
- os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai").replace("http", "ws"),
382
- "/ws/engines/events?version=v2",
383
- )
384
-
385
- return connect(
386
- endpoint,
387
- additional_headers={"Authorization": f"Bearer {api_key}"},
388
- )
389
-
390
-
391
- async def __emit_message(event_type: str, payload: str, topic: str | None = None) -> None:
392
- """Send a message via WebSocket asynchronously."""
393
- global ws_connection_for_sending # noqa: PLW0602
394
- if ws_connection_for_sending is None:
395
- logger.warning("WebSocket connection not available for sending message")
396
- return
397
-
398
- try:
399
- # Determine topic based on session_id and engine_id in the payload
400
- if topic is None:
401
- topic = _determine_response_topic()
402
-
403
- body = {"type": event_type, "payload": json.loads(payload), "topic": topic}
404
-
405
- await ws_connection_for_sending.send(json.dumps(body))
406
- except WebSocketException as e:
407
- logger.error("Error sending event to Nodes API: %s", e)
408
- except Exception as e:
409
- logger.error("Unexpected error while sending event to Nodes API: %s", e)
410
-
411
-
412
- def _determine_response_topic() -> str | None:
413
- """Determine the response topic based on session_id and engine_id in the payload."""
414
- engine_id = GriptapeNodes.get_engine_id()
415
- session_id = GriptapeNodes.get_session_id()
416
-
417
- # Normal topic determination logic
418
- # Check for session_id first (highest priority)
419
- if session_id:
420
- return f"sessions/{session_id}/response"
421
-
422
- # Check for engine_id if no session_id
423
- if engine_id:
424
- return f"engines/{engine_id}/response"
425
-
426
- # Default to generic response topic
427
- return "response"
428
-
429
-
430
- async def __subscribe_to_topic(ws_connection: Any, topic: str) -> None:
431
- """Subscribe to a specific topic in the message bus."""
432
- if ws_connection is None:
433
- logger.warning("WebSocket connection not available for subscribing to topic")
434
- return
435
-
436
- try:
437
- body = {"type": "subscribe", "topic": topic, "payload": {}}
438
- await ws_connection.send(json.dumps(body))
439
- logger.info("Subscribed to topic: %s", topic)
440
- except WebSocketException as e:
441
- logger.error("Error subscribing to topic %s: %s", topic, e)
442
- except Exception as e:
443
- logger.error("Unexpected error while subscribing to topic %s: %s", topic, e)
444
-
445
-
446
- async def __unsubscribe_from_topic(ws_connection: Any, topic: str) -> None:
447
- """Unsubscribe from a specific topic in the message bus."""
448
- if ws_connection is None:
449
- logger.warning("WebSocket connection not available for unsubscribing from topic")
450
- return
451
-
452
- try:
453
- body = {"type": "unsubscribe", "topic": topic, "payload": {}}
454
- await ws_connection.send(json.dumps(body))
455
- logger.info("Unsubscribed from topic: %s", topic)
456
- except WebSocketException as e:
457
- logger.error("Error unsubscribing from topic %s: %s", topic, e)
458
- except Exception as e:
459
- logger.error("Unexpected error while unsubscribing from topic %s: %s", topic, e)
460
-
461
-
462
- async def __subscribe_to_engine_and_session(ws_connection: Any) -> None:
463
- """Subscribe to engine ID, session ID, and request topics on WebSocket connection."""
464
- # Subscribe to request topic (engine discovery)
465
- await __subscribe_to_topic(ws_connection, "request")
466
-
467
- # Get engine ID and subscribe to engine_id/request
468
- engine_id = GriptapeNodes.get_engine_id()
469
- if engine_id:
470
- await __subscribe_to_topic(ws_connection, f"engines/{engine_id}/request")
471
- else:
472
- logger.warning("Engine ID not available for subscription")
473
-
474
- # Get session ID and subscribe to session_id/request if available
475
- session_id = GriptapeNodes.get_session_id()
476
- if session_id:
477
- topic = f"sessions/{session_id}/request"
478
- await __subscribe_to_topic(ws_connection, topic)
479
- logger.info("Subscribed to session topic: %s", topic)
480
- else:
481
- logger.info("No session ID available for subscription")
482
-
483
-
484
- def __schedule_async_task(coro: Any) -> None:
485
- """Schedule an async coroutine to run in the event loop from a sync context."""
486
- if event_loop and event_loop.is_running():
487
- asyncio.run_coroutine_threadsafe(coro, event_loop)
488
- else:
489
- logger.warning("Event loop not available for scheduling async task")
490
-
491
-
492
- def __broadcast_app_initialization_complete(nodes_app_url: str) -> None:
493
- """Broadcast the AppInitializationComplete event to all listeners.
494
-
495
- This is used to notify the GUI that the app is ready to receive events.
496
- """
497
- # Broadcast this to anybody who wants a callback on "hey, the app's ready to roll"
498
- payload = app_events.AppInitializationComplete()
499
- app_event = AppEvent(payload=payload)
500
- __process_app_event(app_event)
501
-
502
- engine_version_request = app_events.GetEngineVersionRequest()
503
- engine_version_result = GriptapeNodes.get_instance().handle_engine_version_request(engine_version_request)
504
- if isinstance(engine_version_result, app_events.GetEngineVersionResultSuccess):
505
- engine_version = f"v{engine_version_result.major}.{engine_version_result.minor}.{engine_version_result.patch}"
506
- else:
507
- engine_version = "<UNKNOWN ENGINE VERSION>"
508
-
509
- # Get current session ID
510
- session_id = GriptapeNodes.get_session_id()
511
- session_info = f" | Session: {session_id[:8]}..." if session_id else " | No Session"
512
-
513
- message = Panel(
514
- Align.center(
515
- f"[bold green]Engine is ready to receive events[/bold green]\n"
516
- f"[bold blue]Return to: [link={nodes_app_url}]{nodes_app_url}[/link] to access the Workflow Editor[/bold blue]",
517
- vertical="middle",
518
- ),
519
- title="🚀 Griptape Nodes Engine Started",
520
- subtitle=f"[green]{engine_version}{session_info}[/green]",
521
- border_style="green",
522
- padding=(1, 4),
523
- )
524
- console.print(message)
525
-
526
-
527
- def __process_api_event(event: dict) -> None:
528
- """Process API events and send them to the event queue."""
529
- payload = event.get("payload", {})
530
-
531
- try:
532
- payload["request"]
533
- except KeyError:
534
- msg = "Error: 'request' was expected but not found."
535
- raise RuntimeError(msg) from None
536
-
537
- try:
538
- event_type = payload["event_type"]
539
- if event_type != "EventRequest":
540
- msg = "Error: 'event_type' was found on request, but did not match 'EventRequest' as expected."
541
- raise RuntimeError(msg) from None
542
- except KeyError:
543
- msg = "Error: 'event_type' not found in request."
544
- raise RuntimeError(msg) from None
545
-
546
- # Now attempt to convert it into an EventRequest.
547
- try:
548
- request_event: EventRequest = cast("EventRequest", deserialize_event(json_data=payload))
549
- except Exception as e:
550
- msg = f"Unable to convert request JSON into a valid EventRequest object. Error Message: '{e}'"
551
- raise RuntimeError(msg) from None
552
-
553
- # Add the event to the queue
554
- event_queue.put(request_event)
@@ -1,78 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: griptape-nodes
3
- Version: 0.42.0
4
- Summary: Add your description here
5
- License-File: LICENSE
6
- Requires-Python: <3.13,>=3.12.0
7
- Requires-Dist: fastapi>=0.115.12
8
- Requires-Dist: griptape[drivers-prompt-amazon-bedrock,drivers-prompt-anthropic,drivers-prompt-cohere,drivers-prompt-ollama,drivers-web-scraper-trafilatura,drivers-web-search-duckduckgo,drivers-web-search-exa,loaders-image]>=1.7.1
9
- Requires-Dist: httpx<1.0.0,>=0.28.0
10
- Requires-Dist: imageio-ffmpeg>=0.6.0
11
- Requires-Dist: json-repair>=0.46.1
12
- Requires-Dist: packaging>=25.0
13
- Requires-Dist: pydantic>=2.10.6
14
- Requires-Dist: python-dotenv>=1.0.1
15
- Requires-Dist: python-multipart>=0.0.20
16
- Requires-Dist: tomlkit>=0.13.2
17
- Requires-Dist: uv>=0.6.16
18
- Requires-Dist: uvicorn>=0.34.2
19
- Requires-Dist: websockets<16.0.0,>=15.0.1
20
- Requires-Dist: xdg-base-dirs>=6.0.2
21
- Provides-Extra: profiling
22
- Requires-Dist: austin-dist>=3.7.0; extra == 'profiling'
23
- Description-Content-Type: text/markdown
24
-
25
- # Griptape Nodes
26
-
27
- Griptape Nodes provides a powerful, visual, node-based interface for building and executing complex AI workflows. It combines a cloud-based IDE with a locally runnable engine, allowing for easy development, debugging, and execution of Griptape applications.
28
-
29
- [![Griptape Nodes Trailer Preview](docs/assets/img/video-thumbnail.jpg)](https://vimeo.com/1064451891)
30
- *(Clicking the image opens the video on Vimeo)*
31
-
32
- **Key Features:**
33
-
34
- - **Visual Workflow Editor:** Design and connect nodes representing different AI tasks, tools, and logic.
35
- - **Local Engine:** Run workflows securely on your own machine or infrastructure.
36
- - **Debugging & Stepping:** Analyze flow execution step-by-step.
37
- - **Scriptable Interface:** Interact with and control flows programmatically.
38
- - **Extensible:** Build your own custom nodes.
39
-
40
- **Learn More:**
41
-
42
- - **Full Documentation:** [docs.griptapenodes.com](https://docs.griptapenodes.com)
43
- - **Installation:** [docs.griptapenodes.com/en/stable/installation/](https://docs.griptapenodes.com/en/latest/installation/)
44
- - **Engine Configuration:** [docs.griptapenodes.com/en/stable/configuration/](https://docs.griptapenodes.com/en/latest/configuration/)
45
-
46
- ______________________________________________________________________
47
-
48
- ## Quick Installation
49
-
50
- Follow these steps to get the Griptape Nodes engine running on your system:
51
-
52
- 1. **Login:** Visit [Griptape Nodes](https://griptapenodes.com) and log in or sign up using your Griptape Cloud credentials.
53
-
54
- 1. **Install Command:** Once logged in, you'll find a setup screen. Copy the installation command provided in the "New Installation" section. It will look similar to this (use the **exact** command provided on the website):
55
-
56
- ```bash
57
- curl -LsSf https://raw.githubusercontent.com/griptape-ai/griptape-nodes/main/install.sh | bash
58
- ```
59
-
60
- 1. **Run Installer:** Open a terminal on your machine (local or cloud environment) and paste/run the command. The installer uses `uv` for fast installation; if `uv` isn't present, the script will typically handle installing it.
61
-
62
- 1. **Initial Configuration (Automatic on First Run):**
63
-
64
- - The first time you run the engine command (`griptape-nodes` or `gtn`), it will guide you through the initial setup:
65
- - **Workspace Directory:** You'll be prompted to choose a directory where Griptape Nodes will store configurations, project files, secrets (`.env`), and generated assets. You can accept the default (`<current_directory>/GriptapeNodes`) or specify a custom path.
66
- - **Griptape Cloud API Key:** Return to the [Griptape Nodes setup page](https://griptapenodes.com) in your browser, click "Generate API Key", copy the key, and paste it when prompted in the terminal.
67
-
68
- 1. **Start the Engine:** After configuration, start the engine by running:
69
-
70
- ```bash
71
- griptape-nodes
72
- ```
73
-
74
- *(or the shorter alias `gtn`)*
75
-
76
- 1. **Connect Workflow Editor:** Refresh the Griptape Nodes Workflow Editor page in your browser. It should now connect to your running engine.
77
-
78
- You're now ready to start building flows! For more detailed setup options and troubleshooting, see the full [Documentation](https://docs.griptapenodes.com/).