griptape-nodes 0.37.0__py3-none-any.whl → 0.38.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. griptape_nodes/__init__.py +292 -132
  2. griptape_nodes/app/__init__.py +1 -6
  3. griptape_nodes/app/app.py +108 -76
  4. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +80 -5
  5. griptape_nodes/drivers/storage/local_storage_driver.py +5 -1
  6. griptape_nodes/exe_types/core_types.py +84 -3
  7. griptape_nodes/exe_types/node_types.py +260 -50
  8. griptape_nodes/machines/node_resolution.py +2 -14
  9. griptape_nodes/retained_mode/events/agent_events.py +7 -0
  10. griptape_nodes/retained_mode/events/base_events.py +16 -0
  11. griptape_nodes/retained_mode/events/library_events.py +26 -0
  12. griptape_nodes/retained_mode/events/parameter_events.py +31 -0
  13. griptape_nodes/retained_mode/griptape_nodes.py +32 -0
  14. griptape_nodes/retained_mode/managers/agent_manager.py +25 -12
  15. griptape_nodes/retained_mode/managers/config_manager.py +37 -4
  16. griptape_nodes/retained_mode/managers/event_manager.py +15 -0
  17. griptape_nodes/retained_mode/managers/flow_manager.py +64 -61
  18. griptape_nodes/retained_mode/managers/library_manager.py +215 -45
  19. griptape_nodes/retained_mode/managers/node_manager.py +344 -147
  20. griptape_nodes/retained_mode/managers/operation_manager.py +6 -0
  21. griptape_nodes/retained_mode/managers/os_manager.py +6 -1
  22. griptape_nodes/retained_mode/managers/secrets_manager.py +7 -2
  23. griptape_nodes/retained_mode/managers/settings.py +2 -11
  24. griptape_nodes/retained_mode/managers/static_files_manager.py +12 -3
  25. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +105 -0
  26. griptape_nodes/retained_mode/managers/workflow_manager.py +4 -4
  27. griptape_nodes/updater/__init__.py +14 -8
  28. griptape_nodes/version_compatibility/__init__.py +1 -0
  29. griptape_nodes/version_compatibility/versions/__init__.py +1 -0
  30. griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py +1 -0
  31. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +77 -0
  32. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/METADATA +4 -1
  33. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/RECORD +36 -33
  34. griptape_nodes/app/app_websocket.py +0 -481
  35. griptape_nodes/app/nodes_api_socket_manager.py +0 -117
  36. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/WHEEL +0 -0
  37. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/entry_points.txt +0 -0
  38. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,481 +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 dotenv import get_key
18
- from fastapi import FastAPI, HTTPException, Request
19
- from fastapi.middleware.cors import CORSMiddleware
20
- from fastapi.staticfiles import StaticFiles
21
- from griptape.events import (
22
- EventBus,
23
- EventListener,
24
- )
25
- from rich.align import Align
26
- from rich.console import Console
27
- from rich.logging import RichHandler
28
- from rich.panel import Panel
29
- from websockets.asyncio.client import connect
30
- from websockets.exceptions import ConnectionClosed, WebSocketException
31
- from xdg_base_dirs import xdg_config_home
32
-
33
- # This import is necessary to register all events, even if not technically used
34
- from griptape_nodes.retained_mode.events import app_events, execution_events
35
- from griptape_nodes.retained_mode.events.base_events import (
36
- AppEvent,
37
- EventRequest,
38
- EventResultFailure,
39
- EventResultSuccess,
40
- ExecutionEvent,
41
- ExecutionGriptapeNodeEvent,
42
- GriptapeNodeEvent,
43
- ProgressEvent,
44
- deserialize_event,
45
- )
46
- from griptape_nodes.retained_mode.events.logger_events import LogHandlerEvent
47
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
48
-
49
- # This is a global event queue that will be used to pass events between threads
50
- event_queue = Queue()
51
-
52
- # Global WebSocket connection for sending events
53
- ws_connection_for_sending = None
54
- event_loop = None
55
-
56
- # Whether to enable the static server
57
- STATIC_SERVER_ENABLED = os.getenv("STATIC_SERVER_ENABLED", "true").lower() == "true"
58
- # Host of the static server
59
- STATIC_SERVER_HOST = os.getenv("STATIC_SERVER_HOST", "localhost")
60
- # Port of the static server
61
- STATIC_SERVER_PORT = int(os.getenv("STATIC_SERVER_PORT", "8124"))
62
- # URL path for the static server
63
- STATIC_SERVER_URL = os.getenv("STATIC_SERVER_URL", "/static")
64
- # Log level for the static server
65
- STATIC_SERVER_LOG_LEVEL = os.getenv("STATIC_SERVER_LOG_LEVEL", "info").lower()
66
-
67
-
68
- class EventLogHandler(logging.Handler):
69
- """Custom logging handler that emits log messages as AppEvents.
70
-
71
- This is used to forward log messages to the event queue so they can be sent to the GUI.
72
- """
73
-
74
- def emit(self, record: logging.LogRecord) -> None:
75
- event_queue.put(
76
- AppEvent(
77
- payload=LogHandlerEvent(message=record.getMessage(), levelname=record.levelname, created=record.created)
78
- )
79
- )
80
-
81
-
82
- # Logger for this module. Important that this is not the same as the griptape_nodes logger or else we'll have infinite log events.
83
- logger = logging.getLogger("griptape_nodes_app")
84
- console = Console()
85
-
86
-
87
- def start_app() -> None:
88
- """Main entry point for the Griptape Nodes app.
89
-
90
- Starts the event loop and listens for events from the Nodes API.
91
- """
92
- _init_event_listeners()
93
-
94
- griptape_nodes_logger = logging.getLogger("griptape_nodes")
95
- # When running as an app, we want to forward all log messages to the event queue so they can be sent to the GUI
96
- griptape_nodes_logger.addHandler(EventLogHandler())
97
- griptape_nodes_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
98
- griptape_nodes_logger.setLevel(logging.INFO)
99
-
100
- # Listen for any signals to exit the app
101
- for sig in (signal.SIGINT, signal.SIGTERM):
102
- signal.signal(sig, lambda *_: sys.exit(0))
103
-
104
- # SSE subscription pushes events into event_queue
105
- threading.Thread(target=_listen_for_api_events, daemon=True).start()
106
-
107
- if STATIC_SERVER_ENABLED:
108
- threading.Thread(target=_serve_static_server, daemon=True).start()
109
-
110
- _process_event_queue()
111
-
112
-
113
- def _serve_static_server() -> None:
114
- """Run FastAPI with Uvicorn in order to serve static files produced by nodes."""
115
- config_manager = GriptapeNodes.ConfigManager()
116
- app = FastAPI()
117
-
118
- static_dir = config_manager.workspace_path / config_manager.merged_config["static_files_directory"]
119
-
120
- if not static_dir.exists():
121
- static_dir.mkdir(parents=True, exist_ok=True)
122
-
123
- app.add_middleware(
124
- CORSMiddleware,
125
- allow_origins=[
126
- os.getenv("GRIPTAPE_NODES_UI_BASE_URL", "https://nodes.griptape.ai"),
127
- "https://nodes-staging.griptape.ai",
128
- "http://localhost:5173",
129
- ],
130
- allow_credentials=True,
131
- allow_methods=["OPTIONS", "GET", "POST", "PUT"],
132
- allow_headers=["*"],
133
- )
134
-
135
- app.mount(
136
- STATIC_SERVER_URL,
137
- StaticFiles(directory=static_dir),
138
- name="static",
139
- )
140
-
141
- @app.post("/static-upload-urls")
142
- async def create_static_file_upload_url(request: Request) -> dict:
143
- """Create a URL for uploading a static file.
144
-
145
- Similar to a presigned URL, but for uploading files to the static server.
146
- """
147
- base_url = request.base_url
148
- body = await request.json()
149
- file_name = body["file_name"]
150
- url = urljoin(str(base_url), f"/static-uploads/{file_name}")
151
-
152
- return {"url": url}
153
-
154
- @app.put("/static-uploads/{file_name:str}")
155
- async def create_static_file(request: Request, file_name: str) -> dict:
156
- """Upload a static file to the static server."""
157
- if not STATIC_SERVER_ENABLED:
158
- msg = "Static server is not enabled. Please set STATIC_SERVER_ENABLED to True."
159
- raise ValueError(msg)
160
-
161
- if not static_dir.exists():
162
- static_dir.mkdir(parents=True, exist_ok=True)
163
- data = await request.body()
164
- try:
165
- Path(static_dir / file_name).write_bytes(data)
166
- except binascii.Error as e:
167
- msg = f"Invalid base64 encoding for file {file_name}."
168
- logger.error(msg)
169
- raise HTTPException(status_code=400, detail=msg) from e
170
- except (OSError, PermissionError) as e:
171
- msg = f"Failed to write file {file_name} to {config_manager.workspace_path}: {e}"
172
- logger.error(msg)
173
- raise HTTPException(status_code=500, detail=msg) from e
174
-
175
- static_url = f"http://{STATIC_SERVER_HOST}:{STATIC_SERVER_PORT}{STATIC_SERVER_URL}/{file_name}"
176
- return {"url": static_url}
177
-
178
- @app.post("/engines/request")
179
- async def create_event(request: Request) -> None:
180
- body = await request.json()
181
- if "payload" in body:
182
- __process_api_event(body["payload"])
183
-
184
- logging.getLogger("uvicorn").addHandler(
185
- RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True)
186
- )
187
-
188
- uvicorn.run(
189
- app, host=STATIC_SERVER_HOST, port=STATIC_SERVER_PORT, log_level=STATIC_SERVER_LOG_LEVEL, log_config=None
190
- )
191
-
192
-
193
- def _init_event_listeners() -> None:
194
- """Set up the Griptape EventBus EventListeners."""
195
- EventBus.add_event_listener(
196
- event_listener=EventListener(on_event=__process_node_event, event_types=[GriptapeNodeEvent])
197
- )
198
-
199
- EventBus.add_event_listener(
200
- event_listener=EventListener(
201
- on_event=__process_execution_node_event,
202
- event_types=[ExecutionGriptapeNodeEvent],
203
- )
204
- )
205
-
206
- EventBus.add_event_listener(
207
- event_listener=EventListener(
208
- on_event=__process_progress_event,
209
- event_types=[ProgressEvent],
210
- )
211
- )
212
-
213
- EventBus.add_event_listener(
214
- event_listener=EventListener(
215
- on_event=__process_app_event, # pyright: ignore[reportArgumentType] TODO: https://github.com/griptape-ai/griptape-nodes/issues/868
216
- event_types=[AppEvent], # pyright: ignore[reportArgumentType] TODO: https://github.com/griptape-ai/griptape-nodes/issues/868
217
- )
218
- )
219
-
220
-
221
- async def _alisten_for_api_requests() -> None:
222
- """Listen for events from the Nodes API and process them asynchronously."""
223
- global ws_connection_for_sending, event_loop # noqa: PLW0603
224
- event_loop = asyncio.get_running_loop() # Store the event loop reference
225
- nodes_app_url = os.getenv("GRIPTAPE_NODES_UI_BASE_URL", "https://nodes.griptape.ai")
226
- logger.info("Listening for events from Nodes API via async WebSocket")
227
-
228
- # Auto reconnect https://websockets.readthedocs.io/en/stable/reference/asyncio/client.html#opening-a-connection
229
- connection_stream = __create_async_websocket_connection()
230
- initialized = False
231
- async for ws_connection in connection_stream:
232
- try:
233
- ws_connection_for_sending = ws_connection # Store for sending events
234
- if not initialized:
235
- __broadcast_app_initialization_complete(nodes_app_url)
236
- initialized = True
237
-
238
- async for message in ws_connection:
239
- try:
240
- data = json.loads(message)
241
-
242
- payload = data.get("payload", {})
243
- # With heartbeat events, we skip the regular processing and just send the heartbeat
244
- # Technically no longer needed since https://github.com/griptape-ai/griptape-nodes/pull/369
245
- # but we don't have a proper EventRequest for it yet.
246
- if payload.get("request_type") == "Heartbeat":
247
- session_id = GriptapeNodes.get_session_id()
248
- await __send_heartbeat(
249
- session_id=session_id, request=payload["request"], ws_connection=ws_connection
250
- )
251
- else:
252
- __process_api_event(payload)
253
- except Exception:
254
- logger.exception("Error processing event, skipping.")
255
- except ConnectionClosed:
256
- continue
257
- except Exception as e:
258
- logger.error("Error while listening for events. Retrying in 2 seconds... %s", e)
259
- await asyncio.sleep(2)
260
-
261
-
262
- def _listen_for_api_events() -> None:
263
- """Run the async WebSocket listener in an event loop."""
264
- asyncio.run(_alisten_for_api_requests())
265
-
266
-
267
- def __process_node_event(event: GriptapeNodeEvent) -> None:
268
- """Process GriptapeNodeEvents and send them to the API."""
269
- # Emit the result back to the GUI
270
- result_event = event.wrapped_event
271
- if isinstance(result_event, EventResultSuccess):
272
- dest_socket = "success_result"
273
- elif isinstance(result_event, EventResultFailure):
274
- dest_socket = "failure_result"
275
- else:
276
- msg = f"Unknown/unsupported result event type encountered: '{type(result_event)}'."
277
- raise TypeError(msg) from None
278
-
279
- # Don't send events over the wire that don't have a request_id set (e.g. engine-internal events)
280
- event_json = result_event.json()
281
- __schedule_async_task(__emit_message(dest_socket, event_json))
282
-
283
-
284
- def __process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
285
- """Process ExecutionGriptapeNodeEvents and send them to the API."""
286
- result_event = event.wrapped_event
287
- if type(result_event.payload).__name__ == "NodeStartProcessEvent":
288
- GriptapeNodes.EventManager().current_active_node = result_event.payload.node_name
289
- event_json = result_event.json()
290
-
291
- if type(result_event.payload).__name__ == "ResumeNodeProcessingEvent":
292
- node_name = result_event.payload.node_name
293
- logger.info("Resuming Node '%s'", node_name)
294
- flow_name = GriptapeNodes.NodeManager().get_node_parent_flow_by_name(node_name)
295
- request = EventRequest(request=execution_events.SingleExecutionStepRequest(flow_name=flow_name))
296
- event_queue.put(request)
297
-
298
- if type(result_event.payload).__name__ == "NodeFinishProcessEvent":
299
- if result_event.payload.node_name != GriptapeNodes.EventManager().current_active_node:
300
- msg = "Node start and finish do not match."
301
- raise KeyError(msg) from None
302
- GriptapeNodes.EventManager().current_active_node = None
303
- __schedule_async_task(__emit_message("execution_event", event_json))
304
-
305
-
306
- def __process_progress_event(gt_event: ProgressEvent) -> None:
307
- """Process Griptape framework events and send them to the API."""
308
- node_name = gt_event.node_name
309
- if node_name:
310
- value = gt_event.value
311
- payload = execution_events.GriptapeEvent(
312
- node_name=node_name, parameter_name=gt_event.parameter_name, type=type(gt_event).__name__, value=value
313
- )
314
- event_to_emit = ExecutionEvent(payload=payload)
315
- __schedule_async_task(__emit_message("execution_event", event_to_emit.json()))
316
-
317
-
318
- def __process_app_event(event: AppEvent) -> None:
319
- """Process AppEvents and send them to the API."""
320
- # Let Griptape Nodes broadcast it.
321
- GriptapeNodes.broadcast_app_event(event.payload)
322
-
323
- __schedule_async_task(__emit_message("app_event", event.json()))
324
-
325
-
326
- def _process_event_queue() -> None:
327
- """Listen for events in the event queue and process them.
328
-
329
- Event queue will be populated by background threads listening for events from the Nodes API.
330
- """
331
- while True:
332
- event = event_queue.get(block=True)
333
- if isinstance(event, EventRequest):
334
- request_payload = event.request
335
- GriptapeNodes.handle_request(request_payload)
336
- elif isinstance(event, AppEvent):
337
- __process_app_event(event)
338
- else:
339
- logger.warning("Unknown event type encountered: '%s'.", type(event))
340
-
341
- event_queue.task_done()
342
-
343
-
344
- def __create_async_websocket_connection() -> Any:
345
- """Create an async WebSocket connection to the Nodes API."""
346
- api_key = get_key(xdg_config_home() / "griptape_nodes" / ".env", "GT_CLOUD_API_KEY")
347
- if api_key is None:
348
- message = Panel(
349
- Align.center(
350
- "[bold red]Nodes API key is not set, please run [code]gtn init[/code] with a valid key: [/bold red]"
351
- "[code]gtn init --api-key <your key>[/code]\n"
352
- "[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]",
353
- ),
354
- title="🔑 ❌ Missing Nodes API Key",
355
- border_style="red",
356
- padding=(1, 4),
357
- )
358
- console.print(message)
359
- sys.exit(1)
360
-
361
- endpoint = urljoin(
362
- os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai").replace("http", "ws"),
363
- "/ws/engines/events?publish_channel=responses&subscribe_channel=requests",
364
- )
365
-
366
- return connect(
367
- endpoint,
368
- additional_headers={"Authorization": f"Bearer {api_key}"},
369
- )
370
-
371
-
372
- async def __emit_message(event_type: str, payload: str) -> None:
373
- """Send a message via WebSocket asynchronously."""
374
- global ws_connection_for_sending # noqa: PLW0602
375
- if ws_connection_for_sending is None:
376
- logger.warning("WebSocket connection not available for sending message")
377
- return
378
-
379
- try:
380
- body = {"type": event_type, "payload": json.loads(payload) if payload else {}}
381
- await ws_connection_for_sending.send(json.dumps(body))
382
- except WebSocketException as e:
383
- logger.error("Error sending event to Nodes API: %s", e)
384
- except Exception as e:
385
- logger.error("Unexpected error while sending event to Nodes API: %s", e)
386
-
387
-
388
- async def __send_heartbeat(*, session_id: str | None, request: dict, ws_connection: Any) -> None:
389
- """Send a heartbeat response via WebSocket."""
390
- heartbeat_response = {
391
- "request": request,
392
- "result": {},
393
- "request_type": "Heartbeat",
394
- "event_type": "EventResultSuccess",
395
- "result_type": "HeartbeatSuccess",
396
- **({"session_id": session_id} if session_id is not None else {}),
397
- }
398
-
399
- body = {"type": "success_result", "payload": heartbeat_response}
400
- try:
401
- await ws_connection.send(json.dumps(body))
402
- logger.debug(
403
- "Responded to heartbeat request with session: %s and request: %s", session_id, request.get("request_id")
404
- )
405
- except WebSocketException as e:
406
- logger.error("Error sending heartbeat response: %s", e)
407
- except Exception as e:
408
- logger.error("Unexpected error while sending heartbeat response: %s", e)
409
-
410
-
411
- def __schedule_async_task(coro: Any) -> None:
412
- """Schedule an async coroutine to run in the event loop from a sync context."""
413
- if event_loop and event_loop.is_running():
414
- asyncio.run_coroutine_threadsafe(coro, event_loop)
415
- else:
416
- logger.warning("Event loop not available for scheduling async task")
417
-
418
-
419
- def __broadcast_app_initialization_complete(nodes_app_url: str) -> None:
420
- """Broadcast the AppInitializationComplete event to all listeners.
421
-
422
- This is used to notify the GUI that the app is ready to receive events.
423
- """
424
- # Broadcast this to anybody who wants a callback on "hey, the app's ready to roll"
425
- payload = app_events.AppInitializationComplete()
426
- app_event = AppEvent(payload=payload)
427
- __process_app_event(app_event)
428
-
429
- engine_version_request = app_events.GetEngineVersionRequest()
430
- engine_version_result = GriptapeNodes.get_instance().handle_engine_version_request(engine_version_request)
431
- if isinstance(engine_version_result, app_events.GetEngineVersionResultSuccess):
432
- engine_version = f"v{engine_version_result.major}.{engine_version_result.minor}.{engine_version_result.patch}"
433
- else:
434
- engine_version = "<UNKNOWN ENGINE VERSION>"
435
-
436
- message = Panel(
437
- Align.center(
438
- f"[bold green]Engine is ready to receive events[/bold green]\n"
439
- f"[bold blue]Return to: [link={nodes_app_url}]{nodes_app_url}[/link] to access the Workflow Editor[/bold blue]",
440
- vertical="middle",
441
- ),
442
- title="🚀 Griptape Nodes Engine Started",
443
- subtitle=f"[green]{engine_version}[/green]",
444
- border_style="green",
445
- padding=(1, 4),
446
- )
447
- console.print(message)
448
-
449
-
450
- def __process_api_event(data: dict) -> None:
451
- """Process API events and send them to the event queue."""
452
- try:
453
- data["request"]
454
- except KeyError:
455
- msg = "Error: 'request' was expected but not found."
456
- raise RuntimeError(msg) from None
457
-
458
- try:
459
- event_type = data["event_type"]
460
- if event_type != "EventRequest":
461
- msg = "Error: 'event_type' was found on request, but did not match 'EventRequest' as expected."
462
- raise RuntimeError(msg) from None
463
- except KeyError:
464
- msg = "Error: 'event_type' not found in request."
465
- raise RuntimeError(msg) from None
466
-
467
- # Now attempt to convert it into an EventRequest.
468
- try:
469
- request_event: EventRequest = cast("EventRequest", deserialize_event(json_data=data))
470
- except Exception as e:
471
- msg = f"Unable to convert request JSON into a valid EventRequest object. Error Message: '{e}'"
472
- raise RuntimeError(msg) from None
473
-
474
- # Add a request_id to the payload
475
- request_id = request_event.request.request_id
476
- request_event.request.request_id = request_id
477
-
478
- # Add the event to the queue
479
- event_queue.put(request_event)
480
-
481
- return request_id
@@ -1,117 +0,0 @@
1
- import json
2
- import logging
3
- import os
4
- import sys
5
- from threading import Lock
6
- from time import sleep
7
- from urllib.parse import urljoin
8
-
9
- from attrs import Factory, define, field
10
- from dotenv import get_key
11
- from rich.align import Align
12
- from rich.console import Console
13
- from rich.panel import Panel
14
- from websockets.exceptions import InvalidStatus, WebSocketException
15
- from websockets.sync.client import ClientConnection, connect
16
- from xdg_base_dirs import xdg_config_home
17
-
18
- console = Console()
19
-
20
- logger = logging.getLogger(__name__)
21
-
22
-
23
- @define(kw_only=True)
24
- class NodesApiSocketManager:
25
- """Drop-in replacement for SocketIO that sends events to the Nodes API via websocket."""
26
-
27
- socket: ClientConnection = field(
28
- default=Factory(
29
- lambda self: self._connect(),
30
- takes_self=True,
31
- ),
32
- )
33
- lock: Lock = field(factory=Lock)
34
-
35
- def emit(self, *args, **kwargs) -> None: # noqa: ARG002 # drop-in replacement workaround
36
- body = {"type": args[0], "payload": json.loads(args[1]) if len(args) > 1 else {}}
37
- sent = False
38
- while not sent:
39
- try:
40
- self.socket.send(json.dumps(body))
41
- sent = True
42
- except WebSocketException as e:
43
- logger.error("Error sending event to Nodes API, attempting to reconnect. %s", e)
44
- self.socket = self._connect()
45
-
46
- def heartbeat(self, *, session_id: str | None, request: dict) -> None:
47
- self.emit(
48
- "success_result",
49
- json.dumps(
50
- {
51
- "request": request,
52
- "result": {},
53
- "request_type": "Heartbeat",
54
- "event_type": "EventResultSuccess",
55
- "result_type": "HeartbeatSuccess",
56
- **({"session_id": session_id} if session_id is not None else {}),
57
- }
58
- ),
59
- )
60
- logger.debug(
61
- "Responded to heartbeat request with session: %s and request: %s", session_id, request.get("request_id")
62
- )
63
-
64
- def run(self, *args, **kwargs) -> None:
65
- pass
66
-
67
- def start_background_task(self, *args, **kwargs) -> None:
68
- pass
69
-
70
- def _connect(self) -> ClientConnection:
71
- while True:
72
- try:
73
- api_key = get_key(xdg_config_home() / "griptape_nodes" / ".env", "GT_CLOUD_API_KEY")
74
- if api_key is None:
75
- message = Panel(
76
- Align.center(
77
- "[bold red]Nodes API key is not set, please run [code]gtn init[/code] with a valid key: [/bold red]"
78
- "[code]gtn init --api-key <your key>[/code]\n"
79
- "[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]",
80
- ),
81
- title="🔑 ❌ Missing Nodes API Key",
82
- border_style="red",
83
- padding=(1, 4),
84
- )
85
- console.print(message)
86
- sys.exit(1)
87
-
88
- return connect(
89
- urljoin(
90
- os.getenv("GRIPTAPE_NODES_API_BASE_URL", "wss://api.nodes.griptape.ai")
91
- .replace("http", "ws")
92
- .replace("https", "wss"),
93
- "/api/editors/ws", # TODO: https://github.com/griptape-ai/griptape-nodes/issues/866
94
- ),
95
- additional_headers={"Authorization": f"Bearer {api_key}"},
96
- ping_timeout=None,
97
- )
98
- except ConnectionError:
99
- logger.warning("Nodes API is not available, waiting 5 seconds before retrying")
100
- logger.debug("Error: ", exc_info=True)
101
- sleep(5)
102
- except InvalidStatus as e:
103
- message = Panel(
104
- Align.center(
105
- f"[bold red]Nodes API key is invalid ({e.response.status_code}), please re-run [code]gtn init[/code] with a valid key: [/bold red]"
106
- "[code]gtn init --api-key <your key>[/code]\n"
107
- "[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]",
108
- ),
109
- title="🔑 ❌ Invalid Nodes API Key",
110
- border_style="red",
111
- padding=(1, 4),
112
- )
113
- console.print(message)
114
- sys.exit(1)
115
- except Exception:
116
- logger.exception("Unexpected error while connecting to Nodes API")
117
- sys.exit(1)