griptape_nodes 0.26.4__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.
- griptape_nodes/__init__.py +446 -0
- griptape_nodes/app/.python-version +1 -0
- griptape_nodes/app/__init__.py +5 -0
- griptape_nodes/app/app.py +400 -0
- griptape_nodes/app/nodes_api_socket_manager.py +115 -0
- griptape_nodes/exe_types/__init__.py +1 -0
- griptape_nodes/exe_types/connections.py +214 -0
- griptape_nodes/exe_types/core_types.py +1304 -0
- griptape_nodes/exe_types/flow.py +443 -0
- griptape_nodes/exe_types/node_types.py +609 -0
- griptape_nodes/exe_types/type_validator.py +35 -0
- griptape_nodes/machines/__init__.py +1 -0
- griptape_nodes/machines/control_flow.py +209 -0
- griptape_nodes/machines/fsm.py +55 -0
- griptape_nodes/machines/node_resolution.py +453 -0
- griptape_nodes/node_library/__init__.py +1 -0
- griptape_nodes/node_library/library_registry.py +222 -0
- griptape_nodes/node_library/workflow_registry.py +111 -0
- griptape_nodes/py.typed +0 -0
- griptape_nodes/retained_mode/__init__.py +1 -0
- griptape_nodes/retained_mode/events/__init__.py +1 -0
- griptape_nodes/retained_mode/events/app_events.py +72 -0
- griptape_nodes/retained_mode/events/arbitrary_python_events.py +30 -0
- griptape_nodes/retained_mode/events/base_events.py +559 -0
- griptape_nodes/retained_mode/events/config_events.py +120 -0
- griptape_nodes/retained_mode/events/connection_events.py +90 -0
- griptape_nodes/retained_mode/events/execution_events.py +274 -0
- griptape_nodes/retained_mode/events/flow_events.py +223 -0
- griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +30 -0
- griptape_nodes/retained_mode/events/library_events.py +178 -0
- griptape_nodes/retained_mode/events/logger_events.py +14 -0
- griptape_nodes/retained_mode/events/node_events.py +262 -0
- griptape_nodes/retained_mode/events/object_events.py +47 -0
- griptape_nodes/retained_mode/events/os_events.py +27 -0
- griptape_nodes/retained_mode/events/parameter_events.py +292 -0
- griptape_nodes/retained_mode/events/payload_registry.py +60 -0
- griptape_nodes/retained_mode/events/secrets_events.py +83 -0
- griptape_nodes/retained_mode/events/static_file_events.py +35 -0
- griptape_nodes/retained_mode/events/validation_events.py +53 -0
- griptape_nodes/retained_mode/events/workflow_events.py +176 -0
- griptape_nodes/retained_mode/griptape_nodes.py +538 -0
- griptape_nodes/retained_mode/managers/__init__.py +1 -0
- griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +36 -0
- griptape_nodes/retained_mode/managers/config_manager.py +446 -0
- griptape_nodes/retained_mode/managers/context_manager.py +401 -0
- griptape_nodes/retained_mode/managers/event_manager.py +123 -0
- griptape_nodes/retained_mode/managers/flow_manager.py +1278 -0
- griptape_nodes/retained_mode/managers/library_manager.py +872 -0
- griptape_nodes/retained_mode/managers/node_manager.py +1749 -0
- griptape_nodes/retained_mode/managers/object_manager.py +258 -0
- griptape_nodes/retained_mode/managers/operation_manager.py +463 -0
- griptape_nodes/retained_mode/managers/os_manager.py +107 -0
- griptape_nodes/retained_mode/managers/secrets_manager.py +142 -0
- griptape_nodes/retained_mode/managers/settings.py +98 -0
- griptape_nodes/retained_mode/managers/static_files_manager.py +89 -0
- griptape_nodes/retained_mode/managers/workflow_manager.py +924 -0
- griptape_nodes/retained_mode/retained_mode.py +835 -0
- griptape_nodes/traits/__init__.py +1 -0
- griptape_nodes/traits/add_param_button.py +21 -0
- griptape_nodes/traits/button.py +21 -0
- griptape_nodes/traits/clamp.py +36 -0
- griptape_nodes/traits/compare.py +20 -0
- griptape_nodes/traits/compare_images.py +41 -0
- griptape_nodes/traits/minmax.py +43 -0
- griptape_nodes/traits/options.py +34 -0
- griptape_nodes/traits/slider.py +37 -0
- griptape_nodes/traits/trait_registry.py +35 -0
- griptape_nodes/traits/traits.json +4 -0
- griptape_nodes/updater/__init__.py +69 -0
- griptape_nodes/updater/__main__.py +4 -0
- griptape_nodes/utils/__init__.py +1 -0
- griptape_nodes/utils/dict_utils.py +215 -0
- griptape_nodes-0.26.4.dist-info/METADATA +73 -0
- griptape_nodes-0.26.4.dist-info/RECORD +77 -0
- griptape_nodes-0.26.4.dist-info/WHEEL +4 -0
- griptape_nodes-0.26.4.dist-info/entry_points.txt +3 -0
- griptape_nodes-0.26.4.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
from queue import Queue
|
|
10
|
+
from time import sleep
|
|
11
|
+
from typing import Any, cast
|
|
12
|
+
from urllib.parse import urljoin
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
import uvicorn
|
|
16
|
+
from dotenv import get_key
|
|
17
|
+
from fastapi import FastAPI
|
|
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 xdg_base_dirs import xdg_config_home
|
|
29
|
+
|
|
30
|
+
from griptape_nodes.app.nodes_api_socket_manager import NodesApiSocketManager
|
|
31
|
+
|
|
32
|
+
# This import is necessary to register all events, even if not technically used
|
|
33
|
+
from griptape_nodes.retained_mode.events import app_events, execution_events
|
|
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
|
+
# Whether to enable the static server
|
|
52
|
+
STATIC_SERVER_ENABLED = os.getenv("STATIC_SERVER_ENABLED", "true").lower() == "true"
|
|
53
|
+
# Host of the static server
|
|
54
|
+
STATIC_SERVER_HOST = os.getenv("STATIC_SERVER_HOST", "localhost")
|
|
55
|
+
# Port of the static server
|
|
56
|
+
STATIC_SERVER_PORT = int(os.getenv("STATIC_SERVER_PORT", "8124"))
|
|
57
|
+
# URL path for the static server
|
|
58
|
+
STATIC_SERVER_URL = os.getenv("STATIC_SERVER_URL", "/static")
|
|
59
|
+
# Log level for the static server
|
|
60
|
+
STATIC_SERVER_LOG_LEVEL = os.getenv("STATIC_SERVER_LOG_LEVEL", "info").lower()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class EventLogHandler(logging.Handler):
|
|
64
|
+
"""Custom logging handler that emits log messages as AppEvents.
|
|
65
|
+
|
|
66
|
+
This is used to forward log messages to the event queue so they can be sent to the GUI.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
70
|
+
event_queue.put(
|
|
71
|
+
AppEvent(
|
|
72
|
+
payload=LogHandlerEvent(message=record.getMessage(), levelname=record.levelname, created=record.created)
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# When running as an app, we want to forward all log messages to the event queue so they can be sent to the GUI
|
|
78
|
+
logging.getLogger("griptape_nodes").addHandler(EventLogHandler())
|
|
79
|
+
# Logger for this module. Important that this is not the same as the griptape_nodes logger or else we'll have infinite log events.
|
|
80
|
+
logger = logging.getLogger(__name__)
|
|
81
|
+
console = Console()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def start_app() -> None:
|
|
85
|
+
"""Main entry point for the Griptape Nodes app.
|
|
86
|
+
|
|
87
|
+
Starts the event loop and listens for events from the Nodes API.
|
|
88
|
+
"""
|
|
89
|
+
global socket_manager # noqa: PLW0603 # Need to initialize the socket lazily here to avoid auth-ing too early
|
|
90
|
+
|
|
91
|
+
# Listen for SSE events from the Nodes API in a separate thread
|
|
92
|
+
socket_manager = NodesApiSocketManager()
|
|
93
|
+
|
|
94
|
+
_init_event_listeners()
|
|
95
|
+
|
|
96
|
+
# Listen for any signals to exit the app
|
|
97
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
98
|
+
signal.signal(sig, lambda *_: sys.exit(0))
|
|
99
|
+
|
|
100
|
+
# SSE subscription pushes events into event_queue
|
|
101
|
+
threading.Thread(target=_listen_for_api_events, daemon=True).start()
|
|
102
|
+
|
|
103
|
+
if STATIC_SERVER_ENABLED:
|
|
104
|
+
threading.Thread(target=_serve_static_server, daemon=True).start()
|
|
105
|
+
|
|
106
|
+
_process_event_queue()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _serve_static_server() -> None:
|
|
110
|
+
"""Run FastAPI with Uvicorn in order to serve static files produced by nodes."""
|
|
111
|
+
config_manager = GriptapeNodes.ConfigManager()
|
|
112
|
+
app = FastAPI()
|
|
113
|
+
|
|
114
|
+
static_dir = config_manager.workspace_path / config_manager.user_config["static_files_directory"]
|
|
115
|
+
|
|
116
|
+
if not static_dir.exists():
|
|
117
|
+
static_dir.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
|
|
119
|
+
app.add_middleware(
|
|
120
|
+
CORSMiddleware,
|
|
121
|
+
allow_origins=[
|
|
122
|
+
"https://nodes.griptape.ai",
|
|
123
|
+
"http://localhost",
|
|
124
|
+
],
|
|
125
|
+
allow_credentials=True,
|
|
126
|
+
allow_methods=["GET"],
|
|
127
|
+
allow_headers=["*"],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
app.mount(
|
|
131
|
+
STATIC_SERVER_URL,
|
|
132
|
+
StaticFiles(directory=static_dir),
|
|
133
|
+
name="static",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
logging.getLogger("uvicorn").addHandler(
|
|
137
|
+
RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
uvicorn.run(
|
|
141
|
+
app, host=STATIC_SERVER_HOST, port=STATIC_SERVER_PORT, log_level=STATIC_SERVER_LOG_LEVEL, log_config=None
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _init_event_listeners() -> None:
|
|
146
|
+
"""Set up the Griptape EventBus EventListeners."""
|
|
147
|
+
EventBus.add_event_listener(
|
|
148
|
+
event_listener=EventListener(on_event=__process_node_event, event_types=[GriptapeNodeEvent])
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
EventBus.add_event_listener(
|
|
152
|
+
event_listener=EventListener(
|
|
153
|
+
on_event=__process_execution_node_event,
|
|
154
|
+
event_types=[ExecutionGriptapeNodeEvent],
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
EventBus.add_event_listener(
|
|
159
|
+
event_listener=EventListener(
|
|
160
|
+
on_event=__process_progress_event,
|
|
161
|
+
event_types=[ProgressEvent],
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
EventBus.add_event_listener(
|
|
166
|
+
event_listener=EventListener(
|
|
167
|
+
on_event=__process_app_event, # pyright: ignore[reportArgumentType] TODO: https://github.com/griptape-ai/griptape-nodes/issues/868
|
|
168
|
+
event_types=[AppEvent], # pyright: ignore[reportArgumentType] TODO: https://github.com/griptape-ai/griptape-nodes/issues/868
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _listen_for_api_events() -> None:
|
|
174
|
+
"""Listen for events from the Nodes API and process them."""
|
|
175
|
+
init = False
|
|
176
|
+
while True:
|
|
177
|
+
try:
|
|
178
|
+
endpoint = urljoin(
|
|
179
|
+
os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai"), "/api/engines/stream"
|
|
180
|
+
)
|
|
181
|
+
nodes_app_url = os.getenv("GRIPTAPE_NODES_APP_URL", "https://nodes.griptape.ai")
|
|
182
|
+
|
|
183
|
+
with httpx.stream("get", endpoint, auth=__build_authorized_request, timeout=None) as response: # noqa: S113 We intentionally want to never timeout
|
|
184
|
+
__check_api_key_validity(response)
|
|
185
|
+
|
|
186
|
+
response.raise_for_status()
|
|
187
|
+
|
|
188
|
+
for line in response.iter_lines():
|
|
189
|
+
if line.startswith("data:"):
|
|
190
|
+
data = line.removeprefix("data:").strip()
|
|
191
|
+
if data == "START":
|
|
192
|
+
if not init:
|
|
193
|
+
__broadcast_app_initialization_complete(nodes_app_url)
|
|
194
|
+
init = True
|
|
195
|
+
else:
|
|
196
|
+
try:
|
|
197
|
+
event = json.loads(data)
|
|
198
|
+
# With heartbeat events, we skip the regular processing and just send the heartbeat
|
|
199
|
+
# Technically no longer needed since https://github.com/griptape-ai/griptape-nodes/pull/369
|
|
200
|
+
# but we don't have a proper EventRequest for it yet.
|
|
201
|
+
if event.get("request_type") == "Heartbeat":
|
|
202
|
+
session_id = GriptapeNodes.get_session_id()
|
|
203
|
+
socket_manager.heartbeat(session_id=session_id, request=event["request"])
|
|
204
|
+
else:
|
|
205
|
+
__process_api_event(event)
|
|
206
|
+
except Exception:
|
|
207
|
+
logger.exception("Error processing event, skipping.")
|
|
208
|
+
|
|
209
|
+
except Exception as e:
|
|
210
|
+
details = f"Error while listening for events. Retrying in 2 seconds.: {e}"
|
|
211
|
+
logger.error(details)
|
|
212
|
+
sleep(2)
|
|
213
|
+
init = False
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def __process_node_event(event: GriptapeNodeEvent) -> None:
|
|
217
|
+
"""Process GriptapeNodeEvents and send them to the API."""
|
|
218
|
+
# Emit the result back to the GUI
|
|
219
|
+
result_event = event.wrapped_event
|
|
220
|
+
if isinstance(result_event, EventResultSuccess):
|
|
221
|
+
dest_socket = "success_result"
|
|
222
|
+
elif isinstance(result_event, EventResultFailure):
|
|
223
|
+
dest_socket = "failure_result"
|
|
224
|
+
else:
|
|
225
|
+
msg = f"Unknown/unsupported result event type encountered: '{type(result_event)}'."
|
|
226
|
+
raise TypeError(msg) from None
|
|
227
|
+
|
|
228
|
+
# Don't send events over the wire that don't have a request_id set (e.g. engine-internal events)
|
|
229
|
+
event_json = result_event.json()
|
|
230
|
+
socket_manager.emit(dest_socket, event_json)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def __process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
|
|
234
|
+
"""Process ExecutionGriptapeNodeEvents and send them to the API."""
|
|
235
|
+
result_event = event.wrapped_event
|
|
236
|
+
if type(result_event.payload).__name__ == "NodeStartProcessEvent":
|
|
237
|
+
GriptapeNodes.EventManager().current_active_node = result_event.payload.node_name
|
|
238
|
+
event_json = result_event.json()
|
|
239
|
+
|
|
240
|
+
if type(result_event.payload).__name__ == "ResumeNodeProcessingEvent":
|
|
241
|
+
node_name = result_event.payload.node_name
|
|
242
|
+
logger.info("Resuming Node %s", node_name)
|
|
243
|
+
flow_name = GriptapeNodes.NodeManager().get_node_parent_flow_by_name(node_name)
|
|
244
|
+
request = EventRequest(request=execution_events.SingleExecutionStepRequest(flow_name=flow_name))
|
|
245
|
+
event_queue.put(request)
|
|
246
|
+
|
|
247
|
+
if type(result_event.payload).__name__ == "NodeFinishProcessEvent":
|
|
248
|
+
if result_event.payload.node_name != GriptapeNodes.EventManager().current_active_node:
|
|
249
|
+
msg = "Node start and finish do not match."
|
|
250
|
+
raise KeyError(msg) from None
|
|
251
|
+
GriptapeNodes.EventManager().current_active_node = None
|
|
252
|
+
# Set the node name here so I am not double importing
|
|
253
|
+
socket_manager.emit("execution_event", event_json)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def __process_progress_event(gt_event: ProgressEvent) -> None:
|
|
257
|
+
"""Process Griptape framework events and send them to the API."""
|
|
258
|
+
node_name = gt_event.node_name
|
|
259
|
+
if node_name:
|
|
260
|
+
value = gt_event.value
|
|
261
|
+
payload = execution_events.GriptapeEvent(
|
|
262
|
+
node_name=node_name, parameter_name=gt_event.parameter_name, type=type(gt_event).__name__, value=value
|
|
263
|
+
)
|
|
264
|
+
event_to_emit = ExecutionEvent(payload=payload)
|
|
265
|
+
socket_manager.emit("execution_event", event_to_emit.json())
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def __process_app_event(event: AppEvent) -> None:
|
|
269
|
+
"""Process AppEvents and send them to the API."""
|
|
270
|
+
# Let Griptape Nodes broadcast it.
|
|
271
|
+
GriptapeNodes.broadcast_app_event(event.payload)
|
|
272
|
+
|
|
273
|
+
socket_manager.emit("app_event", event.json())
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _process_event_queue() -> None:
|
|
277
|
+
"""Listen for events in the event queue and process them.
|
|
278
|
+
|
|
279
|
+
Event queue will be populated by background threads listening for events from the Nodes API.
|
|
280
|
+
"""
|
|
281
|
+
while True:
|
|
282
|
+
event = event_queue.get(block=True)
|
|
283
|
+
if isinstance(event, EventRequest):
|
|
284
|
+
request_payload = event.request
|
|
285
|
+
GriptapeNodes.handle_request(request_payload)
|
|
286
|
+
elif isinstance(event, AppEvent):
|
|
287
|
+
__process_app_event(event)
|
|
288
|
+
else:
|
|
289
|
+
logger.warning("Unknown event type encountered: '%s'.", type(event))
|
|
290
|
+
|
|
291
|
+
event_queue.task_done()
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def __build_authorized_request(request: httpx.Request) -> httpx.Request:
|
|
295
|
+
api_key = get_key(xdg_config_home() / "griptape_nodes" / ".env", "GT_CLOUD_API_KEY")
|
|
296
|
+
if api_key is None:
|
|
297
|
+
message = Panel(
|
|
298
|
+
Align.center(
|
|
299
|
+
"[bold red]Nodes API key is not set, please run [code]gtn init[/code] with a valid key: [/bold red]"
|
|
300
|
+
"[code]gtn init --api-key <your key>[/code]\n"
|
|
301
|
+
"[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]",
|
|
302
|
+
),
|
|
303
|
+
title="🔑 ❌ Missing Nodes API Key",
|
|
304
|
+
border_style="red",
|
|
305
|
+
padding=(1, 4),
|
|
306
|
+
)
|
|
307
|
+
console.print(message)
|
|
308
|
+
sys.exit(1)
|
|
309
|
+
request.headers.update(
|
|
310
|
+
{
|
|
311
|
+
"Accept": "text/event-stream",
|
|
312
|
+
"Authorization": f"Bearer {api_key}",
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
return request
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def __check_api_key_validity(response: httpx.Response) -> None:
|
|
319
|
+
"""Check if the API key is valid by checking the response status code.
|
|
320
|
+
|
|
321
|
+
If the API key is invalid, print an error message and exit the program.
|
|
322
|
+
"""
|
|
323
|
+
if response.status_code in {401, 403}:
|
|
324
|
+
message = Panel(
|
|
325
|
+
Align.center(
|
|
326
|
+
"[bold red]Nodes API key is invalid, please run [code]gtn init[/code] with a valid key: [/bold red]"
|
|
327
|
+
"[code]gtn init --api-key <your key>[/code]\n"
|
|
328
|
+
"[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]",
|
|
329
|
+
),
|
|
330
|
+
title="🔑 ❌ Invalid Nodes API Key",
|
|
331
|
+
border_style="red",
|
|
332
|
+
padding=(1, 4),
|
|
333
|
+
)
|
|
334
|
+
console.print(message)
|
|
335
|
+
sys.exit(1)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def __broadcast_app_initialization_complete(nodes_app_url: str) -> None:
|
|
339
|
+
"""Broadcast the AppInitializationComplete event to all listeners.
|
|
340
|
+
|
|
341
|
+
This is used to notify the GUI that the app is ready to receive events.
|
|
342
|
+
"""
|
|
343
|
+
# Broadcast this to anybody who wants a callback on "hey, the app's ready to roll"
|
|
344
|
+
payload = app_events.AppInitializationComplete()
|
|
345
|
+
app_event = AppEvent(payload=payload)
|
|
346
|
+
__process_app_event(app_event)
|
|
347
|
+
|
|
348
|
+
engine_version_request = app_events.GetEngineVersionRequest()
|
|
349
|
+
engine_version_result = GriptapeNodes.get_instance().handle_engine_version_request(engine_version_request)
|
|
350
|
+
if isinstance(engine_version_result, app_events.GetEngineVersionResultSuccess):
|
|
351
|
+
engine_version = f"v{engine_version_result.major}.{engine_version_result.minor}.{engine_version_result.patch}"
|
|
352
|
+
else:
|
|
353
|
+
engine_version = "<UNKNOWN ENGINE VERSION>"
|
|
354
|
+
|
|
355
|
+
message = Panel(
|
|
356
|
+
Align.center(
|
|
357
|
+
f"[bold green]Engine is ready to receive events[/bold green]\n"
|
|
358
|
+
f"[bold blue]Return to: [link={nodes_app_url}]{nodes_app_url}[/link] to access the Workflow Editor[/bold blue]",
|
|
359
|
+
vertical="middle",
|
|
360
|
+
),
|
|
361
|
+
title="🚀 Griptape Nodes Engine Started",
|
|
362
|
+
subtitle=f"[green]{engine_version}[/green]",
|
|
363
|
+
border_style="green",
|
|
364
|
+
padding=(1, 4),
|
|
365
|
+
)
|
|
366
|
+
console.print(message)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def __process_api_event(data: Any) -> None:
|
|
370
|
+
"""Process API events and send them to the event queue."""
|
|
371
|
+
try:
|
|
372
|
+
data["request"]
|
|
373
|
+
except KeyError:
|
|
374
|
+
msg = "Error: 'request' was expected but not found."
|
|
375
|
+
raise RuntimeError(msg) from None
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
event_type = data["event_type"]
|
|
379
|
+
if event_type != "EventRequest":
|
|
380
|
+
msg = "Error: 'event_type' was found on request, but did not match 'EventRequest' as expected."
|
|
381
|
+
raise RuntimeError(msg) from None
|
|
382
|
+
except KeyError:
|
|
383
|
+
msg = "Error: 'event_type' not found in request."
|
|
384
|
+
raise RuntimeError(msg) from None
|
|
385
|
+
|
|
386
|
+
# Now attempt to convert it into an EventRequest.
|
|
387
|
+
try:
|
|
388
|
+
request_event: EventRequest = cast("EventRequest", deserialize_event(json_data=data))
|
|
389
|
+
except Exception as e:
|
|
390
|
+
msg = f"Unable to convert request JSON into a valid EventRequest object. Error Message: '{e}'"
|
|
391
|
+
raise RuntimeError(msg) from None
|
|
392
|
+
|
|
393
|
+
# Add a request_id to the payload
|
|
394
|
+
request_id = request_event.request.request_id
|
|
395
|
+
request_event.request.request_id = request_id
|
|
396
|
+
|
|
397
|
+
# Add the event to the queue
|
|
398
|
+
event_queue.put(request_event)
|
|
399
|
+
|
|
400
|
+
return request_id
|
|
@@ -0,0 +1,115 @@
|
|
|
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:
|
|
43
|
+
logger.warning("Error sending event to Nodes API, attempting to reconnect.")
|
|
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
|
+
if e.response.status_code in {401, 403}:
|
|
104
|
+
message = Panel(
|
|
105
|
+
Align.center(
|
|
106
|
+
"[bold red]Nodes API key is invalid, please re-run [code]gtn init[/code] with a valid key: [/bold red]"
|
|
107
|
+
"[code]gtn init --api-key <your key>[/code]\n"
|
|
108
|
+
"[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]",
|
|
109
|
+
),
|
|
110
|
+
title="🔑 ❌ Invalid Nodes API Key",
|
|
111
|
+
border_style="red",
|
|
112
|
+
padding=(1, 4),
|
|
113
|
+
)
|
|
114
|
+
console.print(message)
|
|
115
|
+
sys.exit(1)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Executable types package."""
|