griptape-nodes 0.38.1__py3-none-any.whl → 0.41.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.
- griptape_nodes/__init__.py +13 -9
- griptape_nodes/app/__init__.py +10 -1
- griptape_nodes/app/app.py +2 -3
- griptape_nodes/app/app_sessions.py +458 -0
- griptape_nodes/bootstrap/workflow_executors/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +213 -0
- griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +13 -0
- griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +1 -1
- griptape_nodes/drivers/storage/__init__.py +4 -0
- griptape_nodes/drivers/storage/storage_backend.py +10 -0
- griptape_nodes/exe_types/core_types.py +5 -1
- griptape_nodes/exe_types/node_types.py +20 -24
- griptape_nodes/machines/node_resolution.py +5 -1
- griptape_nodes/node_library/advanced_node_library.py +51 -0
- griptape_nodes/node_library/library_registry.py +28 -2
- griptape_nodes/node_library/workflow_registry.py +1 -1
- griptape_nodes/retained_mode/events/agent_events.py +15 -2
- griptape_nodes/retained_mode/events/app_events.py +113 -2
- griptape_nodes/retained_mode/events/base_events.py +28 -1
- griptape_nodes/retained_mode/events/library_events.py +111 -1
- griptape_nodes/retained_mode/events/node_events.py +1 -0
- griptape_nodes/retained_mode/events/workflow_events.py +1 -0
- griptape_nodes/retained_mode/griptape_nodes.py +240 -18
- griptape_nodes/retained_mode/managers/agent_manager.py +123 -17
- griptape_nodes/retained_mode/managers/flow_manager.py +16 -48
- griptape_nodes/retained_mode/managers/library_manager.py +642 -121
- griptape_nodes/retained_mode/managers/node_manager.py +2 -2
- griptape_nodes/retained_mode/managers/static_files_manager.py +4 -3
- griptape_nodes/retained_mode/managers/workflow_manager.py +666 -37
- griptape_nodes/retained_mode/utils/__init__.py +1 -0
- griptape_nodes/retained_mode/utils/engine_identity.py +131 -0
- griptape_nodes/retained_mode/utils/name_generator.py +162 -0
- griptape_nodes/retained_mode/utils/session_persistence.py +105 -0
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.41.0.dist-info}/METADATA +1 -1
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.41.0.dist-info}/RECORD +38 -28
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.41.0.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.41.0.dist-info}/entry_points.txt +0 -0
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.41.0.dist-info}/licenses/LICENSE +0 -0
griptape_nodes/__init__.py
CHANGED
|
@@ -18,7 +18,6 @@ with console.status("Loading Griptape Nodes...") as status:
|
|
|
18
18
|
from typing import Any, Literal
|
|
19
19
|
|
|
20
20
|
import httpx
|
|
21
|
-
from dotenv import load_dotenv
|
|
22
21
|
from rich.box import HEAVY_EDGE
|
|
23
22
|
from rich.panel import Panel
|
|
24
23
|
from rich.progress import Progress
|
|
@@ -27,6 +26,7 @@ with console.status("Loading Griptape Nodes...") as status:
|
|
|
27
26
|
from xdg_base_dirs import xdg_config_home, xdg_data_home
|
|
28
27
|
|
|
29
28
|
from griptape_nodes.app import start_app
|
|
29
|
+
from griptape_nodes.drivers.storage import StorageBackend
|
|
30
30
|
from griptape_nodes.drivers.storage.griptape_cloud_storage_driver import GriptapeCloudStorageDriver
|
|
31
31
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes, engine_version
|
|
32
32
|
from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
|
|
@@ -83,8 +83,6 @@ os_manager = OSManager()
|
|
|
83
83
|
|
|
84
84
|
def main() -> None:
|
|
85
85
|
"""Main entry point for the Griptape Nodes CLI."""
|
|
86
|
-
load_dotenv(ENV_FILE, override=True)
|
|
87
|
-
|
|
88
86
|
# Hack to make paths "just work". # noqa: FIX004
|
|
89
87
|
# Without this, packages like `nodes` don't properly import.
|
|
90
88
|
# Long term solution could be to make `nodes` a proper src-layout package
|
|
@@ -159,7 +157,7 @@ def _handle_bucket_config(config: InitConfig, storage_backend: str | None) -> st
|
|
|
159
157
|
"""Handle bucket configuration step (depends on API key and storage backend)."""
|
|
160
158
|
storage_backend_bucket_id = None
|
|
161
159
|
|
|
162
|
-
if storage_backend ==
|
|
160
|
+
if storage_backend == StorageBackend.GTC.value:
|
|
163
161
|
if config.interactive:
|
|
164
162
|
storage_backend_bucket_id = _prompt_for_gtc_bucket_name(default_bucket_name=config.bucket_name)
|
|
165
163
|
elif config.bucket_name is not None:
|
|
@@ -286,7 +284,7 @@ def _get_args() -> argparse.Namespace:
|
|
|
286
284
|
init_parser.add_argument(
|
|
287
285
|
"--storage-backend",
|
|
288
286
|
help="Set the storage backend ('local' or 'gtc').",
|
|
289
|
-
choices=
|
|
287
|
+
choices=list(StorageBackend),
|
|
290
288
|
default=ENV_STORAGE_BACKEND,
|
|
291
289
|
)
|
|
292
290
|
init_parser.add_argument(
|
|
@@ -429,7 +427,7 @@ Enter 'gtc' to use Griptape Cloud Bucket Storage, or press Return to accept the
|
|
|
429
427
|
try:
|
|
430
428
|
storage_backend = Prompt.ask(
|
|
431
429
|
"Storage Backend",
|
|
432
|
-
choices=
|
|
430
|
+
choices=list(StorageBackend),
|
|
433
431
|
default=default_storage_backend,
|
|
434
432
|
show_default=True,
|
|
435
433
|
)
|
|
@@ -703,8 +701,8 @@ def _sync_libraries() -> None:
|
|
|
703
701
|
extracted_libs = extracted_root / "libraries"
|
|
704
702
|
|
|
705
703
|
# Copy directories from synced libraries without removing existing content
|
|
704
|
+
console.print(f"[green]Syncing libraries to {dest_nodes.resolve()}...[/green]")
|
|
706
705
|
dest_nodes.mkdir(parents=True, exist_ok=True)
|
|
707
|
-
|
|
708
706
|
for library_dir in extracted_libs.iterdir():
|
|
709
707
|
if library_dir.is_dir():
|
|
710
708
|
dest_library_dir = dest_nodes / library_dir.name
|
|
@@ -796,7 +794,7 @@ def _uninstall_self() -> None:
|
|
|
796
794
|
os_manager.replace_process(["uv", "tool", "uninstall", "griptape-nodes"])
|
|
797
795
|
|
|
798
796
|
|
|
799
|
-
def _parse_key_value_pairs(pairs: list[str] | None) -> dict[str,
|
|
797
|
+
def _parse_key_value_pairs(pairs: list[str] | None) -> dict[str, Any] | None:
|
|
800
798
|
"""Parse key=value pairs from a list of strings.
|
|
801
799
|
|
|
802
800
|
Args:
|
|
@@ -822,7 +820,13 @@ def _parse_key_value_pairs(pairs: list[str] | None) -> dict[str, str] | None:
|
|
|
822
820
|
console.print(f"[bold red]Empty key in pair: {pair}[/bold red]")
|
|
823
821
|
continue
|
|
824
822
|
|
|
825
|
-
|
|
823
|
+
# Try to parse value as JSON, fall back to string if it fails
|
|
824
|
+
try:
|
|
825
|
+
parsed_value = json.loads(value)
|
|
826
|
+
result[key] = parsed_value
|
|
827
|
+
except (json.JSONDecodeError, ValueError):
|
|
828
|
+
# If JSON parsing fails, use the original string value
|
|
829
|
+
result[key] = value
|
|
826
830
|
|
|
827
831
|
return result if result else None
|
|
828
832
|
|
griptape_nodes/app/__init__.py
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
"""App package."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
if os.getenv("GTN_USE_SESSIONS", "False").lower() == "true":
|
|
6
|
+
# Sessions are only available in the staging environment
|
|
7
|
+
os.environ["GRIPTAPE_NODES_API_BASE_URL"] = os.getenv(
|
|
8
|
+
"GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes-staging.griptape.ai"
|
|
9
|
+
)
|
|
10
|
+
from griptape_nodes.app.app_sessions import start_app
|
|
11
|
+
else:
|
|
12
|
+
from griptape_nodes.app.app import start_app
|
|
4
13
|
|
|
5
14
|
__all__ = ["start_app"]
|
griptape_nodes/app/app.py
CHANGED
|
@@ -14,7 +14,6 @@ from typing import Any, cast
|
|
|
14
14
|
from urllib.parse import urljoin
|
|
15
15
|
|
|
16
16
|
import uvicorn
|
|
17
|
-
from dotenv import get_key
|
|
18
17
|
from fastapi import FastAPI, HTTPException, Request
|
|
19
18
|
from fastapi.middleware.cors import CORSMiddleware
|
|
20
19
|
from fastapi.staticfiles import StaticFiles
|
|
@@ -28,7 +27,6 @@ from rich.logging import RichHandler
|
|
|
28
27
|
from rich.panel import Panel
|
|
29
28
|
from websockets.asyncio.client import connect
|
|
30
29
|
from websockets.exceptions import ConnectionClosed, WebSocketException
|
|
31
|
-
from xdg_base_dirs import xdg_config_home
|
|
32
30
|
|
|
33
31
|
# This import is necessary to register all events, even if not technically used
|
|
34
32
|
from griptape_nodes.retained_mode.events import app_events, execution_events
|
|
@@ -342,7 +340,8 @@ def _process_event_queue() -> None:
|
|
|
342
340
|
|
|
343
341
|
def __create_async_websocket_connection() -> Any:
|
|
344
342
|
"""Create an async WebSocket connection to the Nodes API."""
|
|
345
|
-
|
|
343
|
+
secrets_manager = GriptapeNodes.SecretsManager()
|
|
344
|
+
api_key = secrets_manager.get_secret("GT_CLOUD_API_KEY")
|
|
346
345
|
if api_key is None:
|
|
347
346
|
message = Panel(
|
|
348
347
|
Align.center(
|
|
@@ -0,0 +1,458 @@
|
|
|
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.base_events import (
|
|
34
|
+
AppEvent,
|
|
35
|
+
EventRequest,
|
|
36
|
+
EventResultFailure,
|
|
37
|
+
EventResultSuccess,
|
|
38
|
+
ExecutionEvent,
|
|
39
|
+
ExecutionGriptapeNodeEvent,
|
|
40
|
+
GriptapeNodeEvent,
|
|
41
|
+
ProgressEvent,
|
|
42
|
+
deserialize_event,
|
|
43
|
+
)
|
|
44
|
+
from griptape_nodes.retained_mode.events.logger_events import LogHandlerEvent
|
|
45
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
46
|
+
|
|
47
|
+
# This is a global event queue that will be used to pass events between threads
|
|
48
|
+
event_queue = Queue()
|
|
49
|
+
|
|
50
|
+
# Global WebSocket connection for sending events
|
|
51
|
+
ws_connection_for_sending = None
|
|
52
|
+
event_loop = None
|
|
53
|
+
|
|
54
|
+
# Whether to enable the static server
|
|
55
|
+
STATIC_SERVER_ENABLED = os.getenv("STATIC_SERVER_ENABLED", "true").lower() == "true"
|
|
56
|
+
# Host of the static server
|
|
57
|
+
STATIC_SERVER_HOST = os.getenv("STATIC_SERVER_HOST", "localhost")
|
|
58
|
+
# Port of the static server
|
|
59
|
+
STATIC_SERVER_PORT = int(os.getenv("STATIC_SERVER_PORT", "8124"))
|
|
60
|
+
# URL path for the static server
|
|
61
|
+
STATIC_SERVER_URL = os.getenv("STATIC_SERVER_URL", "/static")
|
|
62
|
+
# Log level for the static server
|
|
63
|
+
STATIC_SERVER_LOG_LEVEL = os.getenv("STATIC_SERVER_LOG_LEVEL", "info").lower()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class EventLogHandler(logging.Handler):
|
|
67
|
+
"""Custom logging handler that emits log messages as AppEvents.
|
|
68
|
+
|
|
69
|
+
This is used to forward log messages to the event queue so they can be sent to the GUI.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
73
|
+
event_queue.put(
|
|
74
|
+
AppEvent(
|
|
75
|
+
payload=LogHandlerEvent(message=record.getMessage(), levelname=record.levelname, created=record.created)
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Logger for this module. Important that this is not the same as the griptape_nodes logger or else we'll have infinite log events.
|
|
81
|
+
logger = logging.getLogger("griptape_nodes_app")
|
|
82
|
+
console = Console()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def start_app() -> None:
|
|
86
|
+
"""Main entry point for the Griptape Nodes app.
|
|
87
|
+
|
|
88
|
+
Starts the event loop and listens for events from the Nodes API.
|
|
89
|
+
"""
|
|
90
|
+
_init_event_listeners()
|
|
91
|
+
|
|
92
|
+
griptape_nodes_logger = logging.getLogger("griptape_nodes")
|
|
93
|
+
# When running as an app, we want to forward all log messages to the event queue so they can be sent to the GUI
|
|
94
|
+
griptape_nodes_logger.addHandler(EventLogHandler())
|
|
95
|
+
griptape_nodes_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
|
|
96
|
+
griptape_nodes_logger.setLevel(logging.INFO)
|
|
97
|
+
|
|
98
|
+
# Listen for any signals to exit the app
|
|
99
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
100
|
+
signal.signal(sig, lambda *_: sys.exit(0))
|
|
101
|
+
|
|
102
|
+
# SSE subscription pushes events into event_queue
|
|
103
|
+
threading.Thread(target=_listen_for_api_events, daemon=True).start()
|
|
104
|
+
|
|
105
|
+
if STATIC_SERVER_ENABLED:
|
|
106
|
+
threading.Thread(target=_serve_static_server, daemon=True).start()
|
|
107
|
+
|
|
108
|
+
_process_event_queue()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _serve_static_server() -> None:
|
|
112
|
+
"""Run FastAPI with Uvicorn in order to serve static files produced by nodes."""
|
|
113
|
+
config_manager = GriptapeNodes.ConfigManager()
|
|
114
|
+
app = FastAPI()
|
|
115
|
+
|
|
116
|
+
static_dir = config_manager.workspace_path / config_manager.merged_config["static_files_directory"]
|
|
117
|
+
|
|
118
|
+
if not static_dir.exists():
|
|
119
|
+
static_dir.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
|
|
121
|
+
app.add_middleware(
|
|
122
|
+
CORSMiddleware,
|
|
123
|
+
allow_origins=[
|
|
124
|
+
os.getenv("GRIPTAPE_NODES_UI_BASE_URL", "https://app.nodes.griptape.ai"),
|
|
125
|
+
"https://app.nodes-staging.griptape.ai",
|
|
126
|
+
"http://localhost:5173",
|
|
127
|
+
],
|
|
128
|
+
allow_credentials=True,
|
|
129
|
+
allow_methods=["OPTIONS", "GET", "POST", "PUT"],
|
|
130
|
+
allow_headers=["*"],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
app.mount(
|
|
134
|
+
STATIC_SERVER_URL,
|
|
135
|
+
StaticFiles(directory=static_dir),
|
|
136
|
+
name="static",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@app.post("/static-upload-urls")
|
|
140
|
+
async def create_static_file_upload_url(request: Request) -> dict:
|
|
141
|
+
"""Create a URL for uploading a static file.
|
|
142
|
+
|
|
143
|
+
Similar to a presigned URL, but for uploading files to the static server.
|
|
144
|
+
"""
|
|
145
|
+
base_url = request.base_url
|
|
146
|
+
body = await request.json()
|
|
147
|
+
file_name = body["file_name"]
|
|
148
|
+
url = urljoin(str(base_url), f"/static-uploads/{file_name}")
|
|
149
|
+
|
|
150
|
+
return {"url": url}
|
|
151
|
+
|
|
152
|
+
@app.put("/static-uploads/{file_name:str}")
|
|
153
|
+
async def create_static_file(request: Request, file_name: str) -> dict:
|
|
154
|
+
"""Upload a static file to the static server."""
|
|
155
|
+
if not STATIC_SERVER_ENABLED:
|
|
156
|
+
msg = "Static server is not enabled. Please set STATIC_SERVER_ENABLED to True."
|
|
157
|
+
raise ValueError(msg)
|
|
158
|
+
|
|
159
|
+
if not static_dir.exists():
|
|
160
|
+
static_dir.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
data = await request.body()
|
|
162
|
+
try:
|
|
163
|
+
Path(static_dir / file_name).write_bytes(data)
|
|
164
|
+
except binascii.Error as e:
|
|
165
|
+
msg = f"Invalid base64 encoding for file {file_name}."
|
|
166
|
+
logger.error(msg)
|
|
167
|
+
raise HTTPException(status_code=400, detail=msg) from e
|
|
168
|
+
except (OSError, PermissionError) as e:
|
|
169
|
+
msg = f"Failed to write file {file_name} to {config_manager.workspace_path}: {e}"
|
|
170
|
+
logger.error(msg)
|
|
171
|
+
raise HTTPException(status_code=500, detail=msg) from e
|
|
172
|
+
|
|
173
|
+
static_url = f"http://{STATIC_SERVER_HOST}:{STATIC_SERVER_PORT}{STATIC_SERVER_URL}/{file_name}"
|
|
174
|
+
return {"url": static_url}
|
|
175
|
+
|
|
176
|
+
@app.post("/engines/request")
|
|
177
|
+
async def create_event(request: Request) -> None:
|
|
178
|
+
body = await request.json()
|
|
179
|
+
if "payload" in body:
|
|
180
|
+
__process_api_event(body["payload"])
|
|
181
|
+
|
|
182
|
+
logging.getLogger("uvicorn").addHandler(
|
|
183
|
+
RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
uvicorn.run(
|
|
187
|
+
app, host=STATIC_SERVER_HOST, port=STATIC_SERVER_PORT, log_level=STATIC_SERVER_LOG_LEVEL, log_config=None
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _init_event_listeners() -> None:
|
|
192
|
+
"""Set up the Griptape EventBus EventListeners."""
|
|
193
|
+
EventBus.add_event_listener(
|
|
194
|
+
event_listener=EventListener(on_event=__process_node_event, event_types=[GriptapeNodeEvent])
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
EventBus.add_event_listener(
|
|
198
|
+
event_listener=EventListener(
|
|
199
|
+
on_event=__process_execution_node_event,
|
|
200
|
+
event_types=[ExecutionGriptapeNodeEvent],
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
EventBus.add_event_listener(
|
|
205
|
+
event_listener=EventListener(
|
|
206
|
+
on_event=__process_progress_event,
|
|
207
|
+
event_types=[ProgressEvent],
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
EventBus.add_event_listener(
|
|
212
|
+
event_listener=EventListener(
|
|
213
|
+
on_event=__process_app_event, # pyright: ignore[reportArgumentType] TODO: https://github.com/griptape-ai/griptape-nodes/issues/868
|
|
214
|
+
event_types=[AppEvent], # pyright: ignore[reportArgumentType] TODO: https://github.com/griptape-ai/griptape-nodes/issues/868
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def _alisten_for_api_requests() -> None:
|
|
220
|
+
"""Listen for events from the Nodes API and process them asynchronously."""
|
|
221
|
+
global ws_connection_for_sending, event_loop # noqa: PLW0603
|
|
222
|
+
event_loop = asyncio.get_running_loop() # Store the event loop reference
|
|
223
|
+
nodes_app_url = os.getenv("GRIPTAPE_NODES_UI_BASE_URL", "https://nodes.griptape.ai")
|
|
224
|
+
logger.info("Listening for events from Nodes API via async WebSocket")
|
|
225
|
+
|
|
226
|
+
# Auto reconnect https://websockets.readthedocs.io/en/stable/reference/asyncio/client.html#opening-a-connection
|
|
227
|
+
connection_stream = __create_async_websocket_connection()
|
|
228
|
+
initialized = False
|
|
229
|
+
async for ws_connection in connection_stream:
|
|
230
|
+
try:
|
|
231
|
+
ws_connection_for_sending = ws_connection # Store for sending events
|
|
232
|
+
if not initialized:
|
|
233
|
+
__broadcast_app_initialization_complete(nodes_app_url)
|
|
234
|
+
initialized = True
|
|
235
|
+
|
|
236
|
+
async for message in ws_connection:
|
|
237
|
+
try:
|
|
238
|
+
data = json.loads(message)
|
|
239
|
+
|
|
240
|
+
payload = data.get("payload", {})
|
|
241
|
+
__process_api_event(payload)
|
|
242
|
+
except Exception:
|
|
243
|
+
logger.exception("Error processing event, skipping.")
|
|
244
|
+
except ConnectionClosed:
|
|
245
|
+
continue
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.error("Error while listening for events. Retrying in 2 seconds... %s", e)
|
|
248
|
+
await asyncio.sleep(2)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _listen_for_api_events() -> None:
|
|
252
|
+
"""Run the async WebSocket listener in an event loop."""
|
|
253
|
+
asyncio.run(_alisten_for_api_requests())
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def __process_node_event(event: GriptapeNodeEvent) -> None:
|
|
257
|
+
"""Process GriptapeNodeEvents and send them to the API."""
|
|
258
|
+
# Emit the result back to the GUI
|
|
259
|
+
result_event = event.wrapped_event
|
|
260
|
+
if isinstance(result_event, EventResultSuccess):
|
|
261
|
+
dest_socket = "success_result"
|
|
262
|
+
elif isinstance(result_event, EventResultFailure):
|
|
263
|
+
dest_socket = "failure_result"
|
|
264
|
+
else:
|
|
265
|
+
msg = f"Unknown/unsupported result event type encountered: '{type(result_event)}'."
|
|
266
|
+
raise TypeError(msg) from None
|
|
267
|
+
|
|
268
|
+
# Don't send events over the wire that don't have a request_id set (e.g. engine-internal events)
|
|
269
|
+
event_json = result_event.json()
|
|
270
|
+
__schedule_async_task(__emit_message(dest_socket, event_json))
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def __process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
|
|
274
|
+
"""Process ExecutionGriptapeNodeEvents and send them to the API."""
|
|
275
|
+
result_event = event.wrapped_event
|
|
276
|
+
if type(result_event.payload).__name__ == "NodeStartProcessEvent":
|
|
277
|
+
GriptapeNodes.EventManager().current_active_node = result_event.payload.node_name
|
|
278
|
+
event_json = result_event.json()
|
|
279
|
+
|
|
280
|
+
if type(result_event.payload).__name__ == "ResumeNodeProcessingEvent":
|
|
281
|
+
node_name = result_event.payload.node_name
|
|
282
|
+
logger.info("Resuming Node '%s'", node_name)
|
|
283
|
+
flow_name = GriptapeNodes.NodeManager().get_node_parent_flow_by_name(node_name)
|
|
284
|
+
request = EventRequest(request=execution_events.SingleExecutionStepRequest(flow_name=flow_name))
|
|
285
|
+
event_queue.put(request)
|
|
286
|
+
|
|
287
|
+
if type(result_event.payload).__name__ == "NodeFinishProcessEvent":
|
|
288
|
+
if result_event.payload.node_name != GriptapeNodes.EventManager().current_active_node:
|
|
289
|
+
msg = "Node start and finish do not match."
|
|
290
|
+
raise KeyError(msg) from None
|
|
291
|
+
GriptapeNodes.EventManager().current_active_node = None
|
|
292
|
+
__schedule_async_task(__emit_message("execution_event", event_json))
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def __process_progress_event(gt_event: ProgressEvent) -> None:
|
|
296
|
+
"""Process Griptape framework events and send them to the API."""
|
|
297
|
+
node_name = gt_event.node_name
|
|
298
|
+
if node_name:
|
|
299
|
+
value = gt_event.value
|
|
300
|
+
payload = execution_events.GriptapeEvent(
|
|
301
|
+
node_name=node_name, parameter_name=gt_event.parameter_name, type=type(gt_event).__name__, value=value
|
|
302
|
+
)
|
|
303
|
+
event_to_emit = ExecutionEvent(payload=payload)
|
|
304
|
+
__schedule_async_task(__emit_message("execution_event", event_to_emit.json()))
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def __process_app_event(event: AppEvent) -> None:
|
|
308
|
+
"""Process AppEvents and send them to the API."""
|
|
309
|
+
# Let Griptape Nodes broadcast it.
|
|
310
|
+
GriptapeNodes.broadcast_app_event(event.payload)
|
|
311
|
+
|
|
312
|
+
__schedule_async_task(__emit_message("app_event", event.json()))
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _process_event_queue() -> None:
|
|
316
|
+
"""Listen for events in the event queue and process them.
|
|
317
|
+
|
|
318
|
+
Event queue will be populated by background threads listening for events from the Nodes API.
|
|
319
|
+
"""
|
|
320
|
+
while True:
|
|
321
|
+
event = event_queue.get(block=True)
|
|
322
|
+
if isinstance(event, EventRequest):
|
|
323
|
+
request_payload = event.request
|
|
324
|
+
GriptapeNodes.handle_request(request_payload)
|
|
325
|
+
elif isinstance(event, AppEvent):
|
|
326
|
+
__process_app_event(event)
|
|
327
|
+
else:
|
|
328
|
+
logger.warning("Unknown event type encountered: '%s'.", type(event))
|
|
329
|
+
|
|
330
|
+
event_queue.task_done()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def __create_async_websocket_connection() -> Any:
|
|
334
|
+
"""Create an async WebSocket connection to the Nodes API."""
|
|
335
|
+
secrets_manager = GriptapeNodes.SecretsManager()
|
|
336
|
+
api_key = secrets_manager.get_secret("GT_CLOUD_API_KEY")
|
|
337
|
+
if api_key is None:
|
|
338
|
+
message = Panel(
|
|
339
|
+
Align.center(
|
|
340
|
+
"[bold red]Nodes API key is not set, please run [code]gtn init[/code] with a valid key: [/bold red]"
|
|
341
|
+
"[code]gtn init --api-key <your key>[/code]\n"
|
|
342
|
+
"[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]",
|
|
343
|
+
),
|
|
344
|
+
title="🔑 ❌ Missing Nodes API Key",
|
|
345
|
+
border_style="red",
|
|
346
|
+
padding=(1, 4),
|
|
347
|
+
)
|
|
348
|
+
console.print(message)
|
|
349
|
+
sys.exit(1)
|
|
350
|
+
|
|
351
|
+
endpoint = urljoin(
|
|
352
|
+
os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai").replace("http", "ws"),
|
|
353
|
+
"/ws/engines/events?publish_channel=responses&subscribe_channel=requests",
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return connect(
|
|
357
|
+
endpoint,
|
|
358
|
+
additional_headers={"Authorization": f"Bearer {api_key}"},
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
async def __emit_message(event_type: str, payload: str) -> None:
|
|
363
|
+
"""Send a message via WebSocket asynchronously."""
|
|
364
|
+
global ws_connection_for_sending # noqa: PLW0602
|
|
365
|
+
if ws_connection_for_sending is None:
|
|
366
|
+
logger.warning("WebSocket connection not available for sending message")
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
body = {"type": event_type, "payload": json.loads(payload) if payload else {}}
|
|
371
|
+
await ws_connection_for_sending.send(json.dumps(body))
|
|
372
|
+
except WebSocketException as e:
|
|
373
|
+
logger.error("Error sending event to Nodes API: %s", e)
|
|
374
|
+
except Exception as e:
|
|
375
|
+
logger.error("Unexpected error while sending event to Nodes API: %s", e)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def __schedule_async_task(coro: Any) -> None:
|
|
379
|
+
"""Schedule an async coroutine to run in the event loop from a sync context."""
|
|
380
|
+
if event_loop and event_loop.is_running():
|
|
381
|
+
asyncio.run_coroutine_threadsafe(coro, event_loop)
|
|
382
|
+
else:
|
|
383
|
+
logger.warning("Event loop not available for scheduling async task")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def __broadcast_app_initialization_complete(nodes_app_url: str) -> None:
|
|
387
|
+
"""Broadcast the AppInitializationComplete event to all listeners.
|
|
388
|
+
|
|
389
|
+
This is used to notify the GUI that the app is ready to receive events.
|
|
390
|
+
"""
|
|
391
|
+
# Initialize engine ID and persistent data
|
|
392
|
+
from griptape_nodes.retained_mode.events.base_events import BaseEvent
|
|
393
|
+
|
|
394
|
+
BaseEvent.initialize_engine_id()
|
|
395
|
+
BaseEvent.initialize_session_id()
|
|
396
|
+
|
|
397
|
+
# Broadcast this to anybody who wants a callback on "hey, the app's ready to roll"
|
|
398
|
+
payload = app_events.AppInitializationComplete()
|
|
399
|
+
app_event = AppEvent(payload=payload)
|
|
400
|
+
__process_app_event(app_event)
|
|
401
|
+
|
|
402
|
+
engine_version_request = app_events.GetEngineVersionRequest()
|
|
403
|
+
engine_version_result = GriptapeNodes.get_instance().handle_engine_version_request(engine_version_request)
|
|
404
|
+
if isinstance(engine_version_result, app_events.GetEngineVersionResultSuccess):
|
|
405
|
+
engine_version = f"v{engine_version_result.major}.{engine_version_result.minor}.{engine_version_result.patch}"
|
|
406
|
+
else:
|
|
407
|
+
engine_version = "<UNKNOWN ENGINE VERSION>"
|
|
408
|
+
|
|
409
|
+
# Get current session ID
|
|
410
|
+
session_id = GriptapeNodes.get_session_id()
|
|
411
|
+
session_info = f" | Session: {session_id[:8]}..." if session_id else " | No Session"
|
|
412
|
+
|
|
413
|
+
message = Panel(
|
|
414
|
+
Align.center(
|
|
415
|
+
f"[bold green]Engine is ready to receive events[/bold green]\n"
|
|
416
|
+
f"[bold blue]Return to: [link={nodes_app_url}]{nodes_app_url}[/link] to access the Workflow Editor[/bold blue]",
|
|
417
|
+
vertical="middle",
|
|
418
|
+
),
|
|
419
|
+
title="🚀 Griptape Nodes Engine Started",
|
|
420
|
+
subtitle=f"[green]{engine_version}{session_info}[/green]",
|
|
421
|
+
border_style="green",
|
|
422
|
+
padding=(1, 4),
|
|
423
|
+
)
|
|
424
|
+
console.print(message)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def __process_api_event(data: dict) -> None:
|
|
428
|
+
"""Process API events and send them to the event queue."""
|
|
429
|
+
try:
|
|
430
|
+
data["request"]
|
|
431
|
+
except KeyError:
|
|
432
|
+
msg = "Error: 'request' was expected but not found."
|
|
433
|
+
raise RuntimeError(msg) from None
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
event_type = data["event_type"]
|
|
437
|
+
if event_type != "EventRequest":
|
|
438
|
+
msg = "Error: 'event_type' was found on request, but did not match 'EventRequest' as expected."
|
|
439
|
+
raise RuntimeError(msg) from None
|
|
440
|
+
except KeyError:
|
|
441
|
+
msg = "Error: 'event_type' not found in request."
|
|
442
|
+
raise RuntimeError(msg) from None
|
|
443
|
+
|
|
444
|
+
# Now attempt to convert it into an EventRequest.
|
|
445
|
+
try:
|
|
446
|
+
request_event: EventRequest = cast("EventRequest", deserialize_event(json_data=data))
|
|
447
|
+
except Exception as e:
|
|
448
|
+
msg = f"Unable to convert request JSON into a valid EventRequest object. Error Message: '{e}'"
|
|
449
|
+
raise RuntimeError(msg) from None
|
|
450
|
+
|
|
451
|
+
# Add a request_id to the payload
|
|
452
|
+
request_id = request_event.request.request_id
|
|
453
|
+
request_event.request.request_id = request_id
|
|
454
|
+
|
|
455
|
+
# Add the event to the queue
|
|
456
|
+
event_queue.put(request_event)
|
|
457
|
+
|
|
458
|
+
return request_id
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Workflow executors package."""
|