griptape-nodes 0.41.0__py3-none-any.whl → 0.43.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- griptape_nodes/__init__.py +0 -0
- griptape_nodes/app/.python-version +0 -0
- griptape_nodes/app/__init__.py +1 -10
- griptape_nodes/app/api.py +199 -0
- griptape_nodes/app/app.py +140 -222
- griptape_nodes/app/watch.py +4 -2
- griptape_nodes/bootstrap/__init__.py +0 -0
- griptape_nodes/bootstrap/bootstrap_script.py +0 -0
- griptape_nodes/bootstrap/register_libraries_script.py +0 -0
- griptape_nodes/bootstrap/structure_config.yaml +0 -0
- griptape_nodes/bootstrap/workflow_executors/__init__.py +0 -0
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +0 -0
- griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +0 -0
- griptape_nodes/bootstrap/workflow_runners/__init__.py +0 -0
- griptape_nodes/bootstrap/workflow_runners/bootstrap_workflow_runner.py +0 -0
- griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +0 -0
- griptape_nodes/bootstrap/workflow_runners/subprocess_workflow_runner.py +6 -2
- griptape_nodes/bootstrap/workflow_runners/workflow_runner.py +0 -0
- griptape_nodes/drivers/__init__.py +0 -0
- griptape_nodes/drivers/storage/__init__.py +0 -0
- griptape_nodes/drivers/storage/base_storage_driver.py +0 -0
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +0 -0
- griptape_nodes/drivers/storage/local_storage_driver.py +5 -3
- griptape_nodes/drivers/storage/storage_backend.py +0 -0
- griptape_nodes/exe_types/__init__.py +0 -0
- griptape_nodes/exe_types/connections.py +0 -0
- griptape_nodes/exe_types/core_types.py +0 -0
- griptape_nodes/exe_types/flow.py +68 -368
- griptape_nodes/exe_types/node_types.py +17 -1
- griptape_nodes/exe_types/type_validator.py +0 -0
- griptape_nodes/machines/__init__.py +0 -0
- griptape_nodes/machines/control_flow.py +52 -20
- griptape_nodes/machines/fsm.py +16 -2
- griptape_nodes/machines/node_resolution.py +16 -14
- griptape_nodes/mcp_server/__init__.py +1 -0
- griptape_nodes/mcp_server/server.py +126 -0
- griptape_nodes/mcp_server/ws_request_manager.py +268 -0
- griptape_nodes/node_library/__init__.py +0 -0
- griptape_nodes/node_library/advanced_node_library.py +0 -0
- griptape_nodes/node_library/library_registry.py +0 -0
- griptape_nodes/node_library/workflow_registry.py +2 -2
- griptape_nodes/py.typed +0 -0
- griptape_nodes/retained_mode/__init__.py +0 -0
- griptape_nodes/retained_mode/events/__init__.py +0 -0
- griptape_nodes/retained_mode/events/agent_events.py +70 -8
- griptape_nodes/retained_mode/events/app_events.py +137 -12
- griptape_nodes/retained_mode/events/arbitrary_python_events.py +23 -0
- griptape_nodes/retained_mode/events/base_events.py +13 -31
- griptape_nodes/retained_mode/events/config_events.py +87 -11
- griptape_nodes/retained_mode/events/connection_events.py +56 -5
- griptape_nodes/retained_mode/events/context_events.py +27 -4
- griptape_nodes/retained_mode/events/execution_events.py +99 -14
- griptape_nodes/retained_mode/events/flow_events.py +165 -7
- griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +0 -0
- griptape_nodes/retained_mode/events/library_events.py +195 -17
- griptape_nodes/retained_mode/events/logger_events.py +11 -0
- griptape_nodes/retained_mode/events/node_events.py +242 -22
- griptape_nodes/retained_mode/events/object_events.py +40 -4
- griptape_nodes/retained_mode/events/os_events.py +116 -3
- griptape_nodes/retained_mode/events/parameter_events.py +212 -8
- griptape_nodes/retained_mode/events/payload_registry.py +0 -0
- griptape_nodes/retained_mode/events/secrets_events.py +59 -7
- griptape_nodes/retained_mode/events/static_file_events.py +57 -4
- griptape_nodes/retained_mode/events/validation_events.py +39 -4
- griptape_nodes/retained_mode/events/workflow_events.py +188 -17
- griptape_nodes/retained_mode/griptape_nodes.py +89 -363
- griptape_nodes/retained_mode/managers/__init__.py +0 -0
- griptape_nodes/retained_mode/managers/agent_manager.py +49 -23
- griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +0 -0
- griptape_nodes/retained_mode/managers/config_manager.py +0 -0
- griptape_nodes/retained_mode/managers/context_manager.py +0 -0
- griptape_nodes/retained_mode/managers/engine_identity_manager.py +146 -0
- griptape_nodes/retained_mode/managers/event_manager.py +14 -2
- griptape_nodes/retained_mode/managers/flow_manager.py +751 -64
- griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +45 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +191 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +346 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +439 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +17 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +82 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +116 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +352 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +104 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +155 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +18 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +12 -0
- griptape_nodes/retained_mode/managers/library_manager.py +255 -40
- griptape_nodes/retained_mode/managers/node_manager.py +120 -103
- griptape_nodes/retained_mode/managers/object_manager.py +11 -3
- griptape_nodes/retained_mode/managers/operation_manager.py +0 -0
- griptape_nodes/retained_mode/managers/os_manager.py +582 -8
- griptape_nodes/retained_mode/managers/secrets_manager.py +4 -0
- griptape_nodes/retained_mode/managers/session_manager.py +328 -0
- griptape_nodes/retained_mode/managers/settings.py +7 -0
- griptape_nodes/retained_mode/managers/static_files_manager.py +0 -0
- griptape_nodes/retained_mode/managers/version_compatibility_manager.py +2 -2
- griptape_nodes/retained_mode/managers/workflow_manager.py +722 -456
- griptape_nodes/retained_mode/retained_mode.py +44 -0
- griptape_nodes/retained_mode/utils/__init__.py +0 -0
- griptape_nodes/retained_mode/utils/engine_identity.py +141 -27
- griptape_nodes/retained_mode/utils/name_generator.py +0 -0
- griptape_nodes/traits/__init__.py +0 -0
- griptape_nodes/traits/add_param_button.py +0 -0
- griptape_nodes/traits/button.py +0 -0
- griptape_nodes/traits/clamp.py +0 -0
- griptape_nodes/traits/compare.py +0 -0
- griptape_nodes/traits/compare_images.py +0 -0
- griptape_nodes/traits/file_system_picker.py +127 -0
- griptape_nodes/traits/minmax.py +0 -0
- griptape_nodes/traits/options.py +0 -0
- griptape_nodes/traits/slider.py +0 -0
- griptape_nodes/traits/trait_registry.py +0 -0
- griptape_nodes/traits/traits.json +0 -0
- griptape_nodes/updater/__init__.py +2 -2
- griptape_nodes/updater/__main__.py +0 -0
- griptape_nodes/utils/__init__.py +0 -0
- griptape_nodes/utils/dict_utils.py +0 -0
- griptape_nodes/utils/image_preview.py +128 -0
- griptape_nodes/utils/metaclasses.py +0 -0
- griptape_nodes/version_compatibility/__init__.py +0 -0
- griptape_nodes/version_compatibility/versions/__init__.py +0 -0
- griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py +0 -0
- griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +5 -5
- griptape_nodes-0.43.0.dist-info/METADATA +90 -0
- griptape_nodes-0.43.0.dist-info/RECORD +129 -0
- griptape_nodes-0.43.0.dist-info/WHEEL +4 -0
- {griptape_nodes-0.41.0.dist-info → griptape_nodes-0.43.0.dist-info}/entry_points.txt +1 -0
- griptape_nodes/app/app_sessions.py +0 -458
- griptape_nodes/retained_mode/utils/session_persistence.py +0 -105
- griptape_nodes-0.41.0.dist-info/METADATA +0 -78
- griptape_nodes-0.41.0.dist-info/RECORD +0 -112
- griptape_nodes-0.41.0.dist-info/WHEEL +0 -4
- griptape_nodes-0.41.0.dist-info/licenses/LICENSE +0 -201
griptape_nodes/app/app.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import binascii
|
|
5
4
|
import json
|
|
6
5
|
import logging
|
|
7
6
|
import os
|
|
@@ -10,13 +9,9 @@ import sys
|
|
|
10
9
|
import threading
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
from queue import Queue
|
|
13
|
-
from typing import Any
|
|
12
|
+
from typing import Any
|
|
14
13
|
from urllib.parse import urljoin
|
|
15
14
|
|
|
16
|
-
import uvicorn
|
|
17
|
-
from fastapi import FastAPI, HTTPException, Request
|
|
18
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
19
|
-
from fastapi.staticfiles import StaticFiles
|
|
20
15
|
from griptape.events import (
|
|
21
16
|
EventBus,
|
|
22
17
|
EventListener,
|
|
@@ -28,6 +23,8 @@ from rich.panel import Panel
|
|
|
28
23
|
from websockets.asyncio.client import connect
|
|
29
24
|
from websockets.exceptions import ConnectionClosed, WebSocketException
|
|
30
25
|
|
|
26
|
+
from griptape_nodes.mcp_server.server import main as mcp_server
|
|
27
|
+
|
|
31
28
|
# This import is necessary to register all events, even if not technically used
|
|
32
29
|
from griptape_nodes.retained_mode.events import app_events, execution_events
|
|
33
30
|
from griptape_nodes.retained_mode.events.base_events import (
|
|
@@ -39,11 +36,12 @@ from griptape_nodes.retained_mode.events.base_events import (
|
|
|
39
36
|
ExecutionGriptapeNodeEvent,
|
|
40
37
|
GriptapeNodeEvent,
|
|
41
38
|
ProgressEvent,
|
|
42
|
-
deserialize_event,
|
|
43
39
|
)
|
|
44
40
|
from griptape_nodes.retained_mode.events.logger_events import LogHandlerEvent
|
|
45
41
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
46
42
|
|
|
43
|
+
from .api import _process_api_event, start_api
|
|
44
|
+
|
|
47
45
|
# This is a global event queue that will be used to pass events between threads
|
|
48
46
|
event_queue = Queue()
|
|
49
47
|
|
|
@@ -51,16 +49,12 @@ event_queue = Queue()
|
|
|
51
49
|
ws_connection_for_sending = None
|
|
52
50
|
event_loop = None
|
|
53
51
|
|
|
52
|
+
# Event to signal when WebSocket connection is ready
|
|
53
|
+
ws_ready_event = threading.Event()
|
|
54
|
+
|
|
55
|
+
|
|
54
56
|
# Whether to enable the static server
|
|
55
57
|
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
58
|
|
|
65
59
|
|
|
66
60
|
class EventLogHandler(logging.Handler):
|
|
@@ -79,6 +73,13 @@ class EventLogHandler(logging.Handler):
|
|
|
79
73
|
|
|
80
74
|
# 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
75
|
logger = logging.getLogger("griptape_nodes_app")
|
|
76
|
+
|
|
77
|
+
griptape_nodes_logger = logging.getLogger("griptape_nodes")
|
|
78
|
+
# When running as an app, we want to forward all log messages to the event queue so they can be sent to the GUI
|
|
79
|
+
griptape_nodes_logger.addHandler(EventLogHandler())
|
|
80
|
+
griptape_nodes_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
|
|
81
|
+
griptape_nodes_logger.setLevel(logging.INFO)
|
|
82
|
+
|
|
82
83
|
console = Console()
|
|
83
84
|
|
|
84
85
|
|
|
@@ -89,103 +90,45 @@ def start_app() -> None:
|
|
|
89
90
|
"""
|
|
90
91
|
_init_event_listeners()
|
|
91
92
|
|
|
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
93
|
# Listen for any signals to exit the app
|
|
99
94
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
100
95
|
signal.signal(sig, lambda *_: sys.exit(0))
|
|
101
96
|
|
|
102
|
-
|
|
103
|
-
threading.Thread(target=
|
|
97
|
+
api_key = _ensure_api_key()
|
|
98
|
+
threading.Thread(target=mcp_server, args=(api_key,), daemon=True).start()
|
|
99
|
+
threading.Thread(target=_listen_for_api_events, args=(api_key,), daemon=True).start()
|
|
104
100
|
|
|
105
101
|
if STATIC_SERVER_ENABLED:
|
|
106
|
-
|
|
102
|
+
static_dir = _build_static_dir()
|
|
103
|
+
threading.Thread(target=start_api, args=(static_dir, event_queue), daemon=True).start()
|
|
107
104
|
|
|
108
105
|
_process_event_queue()
|
|
109
106
|
|
|
110
107
|
|
|
111
|
-
def
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
],
|
|
128
|
-
allow_credentials=True,
|
|
129
|
-
allow_methods=["OPTIONS", "GET", "POST", "PUT"],
|
|
130
|
-
allow_headers=["*"],
|
|
131
|
-
)
|
|
108
|
+
def _ensure_api_key() -> str:
|
|
109
|
+
secrets_manager = GriptapeNodes.SecretsManager()
|
|
110
|
+
api_key = secrets_manager.get_secret("GT_CLOUD_API_KEY")
|
|
111
|
+
if api_key is None:
|
|
112
|
+
message = Panel(
|
|
113
|
+
Align.center(
|
|
114
|
+
"[bold red]Nodes API key is not set, please run [code]gtn init[/code] with a valid key: [/bold red]"
|
|
115
|
+
"[code]gtn init --api-key <your key>[/code]\n"
|
|
116
|
+
"[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]",
|
|
117
|
+
),
|
|
118
|
+
title="🔑 ❌ Missing Nodes API Key",
|
|
119
|
+
border_style="red",
|
|
120
|
+
padding=(1, 4),
|
|
121
|
+
)
|
|
122
|
+
console.print(message)
|
|
123
|
+
sys.exit(1)
|
|
132
124
|
|
|
133
|
-
|
|
134
|
-
STATIC_SERVER_URL,
|
|
135
|
-
StaticFiles(directory=static_dir),
|
|
136
|
-
name="static",
|
|
137
|
-
)
|
|
125
|
+
return api_key
|
|
138
126
|
|
|
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
127
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
128
|
+
def _build_static_dir() -> Path:
|
|
129
|
+
"""Build the static directory path based on the workspace configuration."""
|
|
130
|
+
config_manager = GriptapeNodes.ConfigManager()
|
|
131
|
+
return Path(config_manager.workspace_path) / config_manager.merged_config["static_files_directory"]
|
|
189
132
|
|
|
190
133
|
|
|
191
134
|
def _init_event_listeners() -> None:
|
|
@@ -216,38 +159,31 @@ def _init_event_listeners() -> None:
|
|
|
216
159
|
)
|
|
217
160
|
|
|
218
161
|
|
|
219
|
-
async def _alisten_for_api_requests() -> None:
|
|
162
|
+
async def _alisten_for_api_requests(api_key: str) -> None:
|
|
220
163
|
"""Listen for events from the Nodes API and process them asynchronously."""
|
|
221
164
|
global ws_connection_for_sending, event_loop # noqa: PLW0603
|
|
222
165
|
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
166
|
logger.info("Listening for events from Nodes API via async WebSocket")
|
|
225
167
|
|
|
226
168
|
# Auto reconnect https://websockets.readthedocs.io/en/stable/reference/asyncio/client.html#opening-a-connection
|
|
227
|
-
connection_stream =
|
|
169
|
+
connection_stream = _create_websocket_connection(api_key)
|
|
228
170
|
initialized = False
|
|
229
171
|
async for ws_connection in connection_stream:
|
|
230
172
|
try:
|
|
231
173
|
ws_connection_for_sending = ws_connection # Store for sending events
|
|
174
|
+
ws_ready_event.set() # Signal that WebSocket is ready for sending
|
|
175
|
+
|
|
232
176
|
if not initialized:
|
|
233
|
-
|
|
177
|
+
event_queue.put(AppEvent(payload=app_events.AppInitializationComplete()))
|
|
234
178
|
initialized = True
|
|
235
179
|
|
|
180
|
+
event_queue.put(AppEvent(payload=app_events.AppConnectionEstablished()))
|
|
181
|
+
|
|
236
182
|
async for message in ws_connection:
|
|
237
183
|
try:
|
|
238
184
|
data = json.loads(message)
|
|
239
185
|
|
|
240
|
-
|
|
241
|
-
# With heartbeat events, we skip the regular processing and just send the heartbeat
|
|
242
|
-
# Technically no longer needed since https://github.com/griptape-ai/griptape-nodes/pull/369
|
|
243
|
-
# but we don't have a proper EventRequest for it yet.
|
|
244
|
-
if payload.get("request_type") == "Heartbeat":
|
|
245
|
-
session_id = GriptapeNodes.get_session_id()
|
|
246
|
-
await __send_heartbeat(
|
|
247
|
-
session_id=session_id, request=payload["request"], ws_connection=ws_connection
|
|
248
|
-
)
|
|
249
|
-
else:
|
|
250
|
-
__process_api_event(payload)
|
|
186
|
+
_process_api_event(data, event_queue)
|
|
251
187
|
except Exception:
|
|
252
188
|
logger.exception("Error processing event, skipping.")
|
|
253
189
|
except ConnectionClosed:
|
|
@@ -257,9 +193,9 @@ async def _alisten_for_api_requests() -> None:
|
|
|
257
193
|
await asyncio.sleep(2)
|
|
258
194
|
|
|
259
195
|
|
|
260
|
-
def _listen_for_api_events() -> None:
|
|
196
|
+
def _listen_for_api_events(api_key: str) -> None:
|
|
261
197
|
"""Run the async WebSocket listener in an event loop."""
|
|
262
|
-
asyncio.run(_alisten_for_api_requests())
|
|
198
|
+
asyncio.run(_alisten_for_api_requests(api_key))
|
|
263
199
|
|
|
264
200
|
|
|
265
201
|
def __process_node_event(event: GriptapeNodeEvent) -> None:
|
|
@@ -273,9 +209,8 @@ def __process_node_event(event: GriptapeNodeEvent) -> None:
|
|
|
273
209
|
else:
|
|
274
210
|
msg = f"Unknown/unsupported result event type encountered: '{type(result_event)}'."
|
|
275
211
|
raise TypeError(msg) from None
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
__schedule_async_task(__emit_message(dest_socket, event_json))
|
|
212
|
+
|
|
213
|
+
__schedule_async_task(__emit_message(dest_socket, result_event.json(), topic=result_event.response_topic))
|
|
279
214
|
|
|
280
215
|
|
|
281
216
|
def __process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
|
|
@@ -283,7 +218,6 @@ def __process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
|
|
|
283
218
|
result_event = event.wrapped_event
|
|
284
219
|
if type(result_event.payload).__name__ == "NodeStartProcessEvent":
|
|
285
220
|
GriptapeNodes.EventManager().current_active_node = result_event.payload.node_name
|
|
286
|
-
event_json = result_event.json()
|
|
287
221
|
|
|
288
222
|
if type(result_event.payload).__name__ == "ResumeNodeProcessingEvent":
|
|
289
223
|
node_name = result_event.payload.node_name
|
|
@@ -297,7 +231,7 @@ def __process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
|
|
|
297
231
|
msg = "Node start and finish do not match."
|
|
298
232
|
raise KeyError(msg) from None
|
|
299
233
|
GriptapeNodes.EventManager().current_active_node = None
|
|
300
|
-
__schedule_async_task(__emit_message("execution_event",
|
|
234
|
+
__schedule_async_task(__emit_message("execution_event", result_event.json()))
|
|
301
235
|
|
|
302
236
|
|
|
303
237
|
def __process_progress_event(gt_event: ProgressEvent) -> None:
|
|
@@ -325,11 +259,16 @@ def _process_event_queue() -> None:
|
|
|
325
259
|
|
|
326
260
|
Event queue will be populated by background threads listening for events from the Nodes API.
|
|
327
261
|
"""
|
|
262
|
+
# Wait for WebSocket connection to be established before processing events
|
|
263
|
+
ws_ready_event.wait()
|
|
264
|
+
|
|
328
265
|
while True:
|
|
329
266
|
event = event_queue.get(block=True)
|
|
330
267
|
if isinstance(event, EventRequest):
|
|
331
268
|
request_payload = event.request
|
|
332
|
-
GriptapeNodes.handle_request(
|
|
269
|
+
GriptapeNodes.handle_request(
|
|
270
|
+
request_payload, response_topic=event.response_topic, request_id=event.request_id
|
|
271
|
+
)
|
|
333
272
|
elif isinstance(event, AppEvent):
|
|
334
273
|
__process_app_event(event)
|
|
335
274
|
else:
|
|
@@ -338,27 +277,11 @@ def _process_event_queue() -> None:
|
|
|
338
277
|
event_queue.task_done()
|
|
339
278
|
|
|
340
279
|
|
|
341
|
-
def
|
|
280
|
+
def _create_websocket_connection(api_key: str) -> Any:
|
|
342
281
|
"""Create an async WebSocket connection to the Nodes API."""
|
|
343
|
-
secrets_manager = GriptapeNodes.SecretsManager()
|
|
344
|
-
api_key = secrets_manager.get_secret("GT_CLOUD_API_KEY")
|
|
345
|
-
if api_key is None:
|
|
346
|
-
message = Panel(
|
|
347
|
-
Align.center(
|
|
348
|
-
"[bold red]Nodes API key is not set, please run [code]gtn init[/code] with a valid key: [/bold red]"
|
|
349
|
-
"[code]gtn init --api-key <your key>[/code]\n"
|
|
350
|
-
"[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]",
|
|
351
|
-
),
|
|
352
|
-
title="🔑 ❌ Missing Nodes API Key",
|
|
353
|
-
border_style="red",
|
|
354
|
-
padding=(1, 4),
|
|
355
|
-
)
|
|
356
|
-
console.print(message)
|
|
357
|
-
sys.exit(1)
|
|
358
|
-
|
|
359
282
|
endpoint = urljoin(
|
|
360
283
|
os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai").replace("http", "ws"),
|
|
361
|
-
"/ws/engines/events?
|
|
284
|
+
"/ws/engines/events?version=v2",
|
|
362
285
|
)
|
|
363
286
|
|
|
364
287
|
return connect(
|
|
@@ -367,7 +290,7 @@ def __create_async_websocket_connection() -> Any:
|
|
|
367
290
|
)
|
|
368
291
|
|
|
369
292
|
|
|
370
|
-
async def __emit_message(event_type: str, payload: str) -> None:
|
|
293
|
+
async def __emit_message(event_type: str, payload: str, topic: str | None = None) -> None:
|
|
371
294
|
"""Send a message via WebSocket asynchronously."""
|
|
372
295
|
global ws_connection_for_sending # noqa: PLW0602
|
|
373
296
|
if ws_connection_for_sending is None:
|
|
@@ -375,7 +298,12 @@ async def __emit_message(event_type: str, payload: str) -> None:
|
|
|
375
298
|
return
|
|
376
299
|
|
|
377
300
|
try:
|
|
378
|
-
|
|
301
|
+
# Determine topic based on session_id and engine_id in the payload
|
|
302
|
+
if topic is None:
|
|
303
|
+
topic = _determine_response_topic()
|
|
304
|
+
|
|
305
|
+
body = {"type": event_type, "payload": json.loads(payload), "topic": topic}
|
|
306
|
+
|
|
379
307
|
await ws_connection_for_sending.send(json.dumps(body))
|
|
380
308
|
except WebSocketException as e:
|
|
381
309
|
logger.error("Error sending event to Nodes API: %s", e)
|
|
@@ -383,97 +311,87 @@ async def __emit_message(event_type: str, payload: str) -> None:
|
|
|
383
311
|
logger.error("Unexpected error while sending event to Nodes API: %s", e)
|
|
384
312
|
|
|
385
313
|
|
|
386
|
-
|
|
387
|
-
"""
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
"result": {},
|
|
391
|
-
"request_type": "Heartbeat",
|
|
392
|
-
"event_type": "EventResultSuccess",
|
|
393
|
-
"result_type": "HeartbeatSuccess",
|
|
394
|
-
**({"session_id": session_id} if session_id is not None else {}),
|
|
395
|
-
}
|
|
314
|
+
def _determine_response_topic() -> str | None:
|
|
315
|
+
"""Determine the response topic based on session_id and engine_id in the payload."""
|
|
316
|
+
engine_id = GriptapeNodes.get_engine_id()
|
|
317
|
+
session_id = GriptapeNodes.get_session_id()
|
|
396
318
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
"Responded to heartbeat request with session: %s and request: %s", session_id, request.get("request_id")
|
|
402
|
-
)
|
|
403
|
-
except WebSocketException as e:
|
|
404
|
-
logger.error("Error sending heartbeat response: %s", e)
|
|
405
|
-
except Exception as e:
|
|
406
|
-
logger.error("Unexpected error while sending heartbeat response: %s", e)
|
|
319
|
+
# Normal topic determination logic
|
|
320
|
+
# Check for session_id first (highest priority)
|
|
321
|
+
if session_id:
|
|
322
|
+
return f"sessions/{session_id}/response"
|
|
407
323
|
|
|
324
|
+
# Check for engine_id if no session_id
|
|
325
|
+
if engine_id:
|
|
326
|
+
return f"engines/{engine_id}/response"
|
|
408
327
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if event_loop and event_loop.is_running():
|
|
412
|
-
asyncio.run_coroutine_threadsafe(coro, event_loop)
|
|
413
|
-
else:
|
|
414
|
-
logger.warning("Event loop not available for scheduling async task")
|
|
328
|
+
# Default to generic response topic
|
|
329
|
+
return "response"
|
|
415
330
|
|
|
416
331
|
|
|
417
|
-
def
|
|
418
|
-
"""
|
|
332
|
+
def _determine_request_topic() -> str | None:
|
|
333
|
+
"""Determine the request topic based on session_id and engine_id in the payload."""
|
|
334
|
+
engine_id = GriptapeNodes.get_engine_id()
|
|
335
|
+
session_id = GriptapeNodes.get_session_id()
|
|
419
336
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
app_event = AppEvent(payload=payload)
|
|
425
|
-
__process_app_event(app_event)
|
|
426
|
-
|
|
427
|
-
engine_version_request = app_events.GetEngineVersionRequest()
|
|
428
|
-
engine_version_result = GriptapeNodes.get_instance().handle_engine_version_request(engine_version_request)
|
|
429
|
-
if isinstance(engine_version_result, app_events.GetEngineVersionResultSuccess):
|
|
430
|
-
engine_version = f"v{engine_version_result.major}.{engine_version_result.minor}.{engine_version_result.patch}"
|
|
431
|
-
else:
|
|
432
|
-
engine_version = "<UNKNOWN ENGINE VERSION>"
|
|
433
|
-
|
|
434
|
-
message = Panel(
|
|
435
|
-
Align.center(
|
|
436
|
-
f"[bold green]Engine is ready to receive events[/bold green]\n"
|
|
437
|
-
f"[bold blue]Return to: [link={nodes_app_url}]{nodes_app_url}[/link] to access the Workflow Editor[/bold blue]",
|
|
438
|
-
vertical="middle",
|
|
439
|
-
),
|
|
440
|
-
title="🚀 Griptape Nodes Engine Started",
|
|
441
|
-
subtitle=f"[green]{engine_version}[/green]",
|
|
442
|
-
border_style="green",
|
|
443
|
-
padding=(1, 4),
|
|
444
|
-
)
|
|
445
|
-
console.print(message)
|
|
337
|
+
# Normal topic determination logic
|
|
338
|
+
# Check for session_id first (highest priority)
|
|
339
|
+
if session_id:
|
|
340
|
+
return f"sessions/{session_id}/request"
|
|
446
341
|
|
|
342
|
+
# Check for engine_id if no session_id
|
|
343
|
+
if engine_id:
|
|
344
|
+
return f"engines/{engine_id}/request"
|
|
447
345
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
346
|
+
# Default to generic request topic
|
|
347
|
+
return "request"
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def subscribe_to_topic(topic: str) -> None:
|
|
351
|
+
"""Subscribe to a specific topic in the message bus."""
|
|
352
|
+
__schedule_async_task(_asubscribe_to_topic(topic))
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def unsubscribe_from_topic(topic: str) -> None:
|
|
356
|
+
"""Unsubscribe from a specific topic in the message bus."""
|
|
357
|
+
__schedule_async_task(_aunsubscribe_from_topic(topic))
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
async def _asubscribe_to_topic(topic: str) -> None:
|
|
361
|
+
"""Subscribe to a specific topic in the message bus."""
|
|
362
|
+
if ws_connection_for_sending is None:
|
|
363
|
+
logger.warning("WebSocket connection not available for subscribing to topic")
|
|
364
|
+
return
|
|
455
365
|
|
|
456
366
|
try:
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
msg = "Error: 'event_type' not found in request."
|
|
463
|
-
raise RuntimeError(msg) from None
|
|
464
|
-
|
|
465
|
-
# Now attempt to convert it into an EventRequest.
|
|
466
|
-
try:
|
|
467
|
-
request_event: EventRequest = cast("EventRequest", deserialize_event(json_data=data))
|
|
367
|
+
body = {"type": "subscribe", "topic": topic, "payload": {}}
|
|
368
|
+
await ws_connection_for_sending.send(json.dumps(body))
|
|
369
|
+
logger.info("Subscribed to topic: %s", topic)
|
|
370
|
+
except WebSocketException as e:
|
|
371
|
+
logger.error("Error subscribing to topic %s: %s", topic, e)
|
|
468
372
|
except Exception as e:
|
|
469
|
-
|
|
470
|
-
|
|
373
|
+
logger.error("Unexpected error while subscribing to topic %s: %s", topic, e)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
async def _aunsubscribe_from_topic(topic: str) -> None:
|
|
377
|
+
"""Unsubscribe from a specific topic in the message bus."""
|
|
378
|
+
if ws_connection_for_sending is None:
|
|
379
|
+
logger.warning("WebSocket connection not available for unsubscribing from topic")
|
|
380
|
+
return
|
|
471
381
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
382
|
+
try:
|
|
383
|
+
body = {"type": "unsubscribe", "topic": topic, "payload": {}}
|
|
384
|
+
await ws_connection_for_sending.send(json.dumps(body))
|
|
385
|
+
logger.info("Unsubscribed from topic: %s", topic)
|
|
386
|
+
except WebSocketException as e:
|
|
387
|
+
logger.error("Error unsubscribing from topic %s: %s", topic, e)
|
|
388
|
+
except Exception as e:
|
|
389
|
+
logger.error("Unexpected error while unsubscribing from topic %s: %s", topic, e)
|
|
475
390
|
|
|
476
|
-
# Add the event to the queue
|
|
477
|
-
event_queue.put(request_event)
|
|
478
391
|
|
|
479
|
-
|
|
392
|
+
def __schedule_async_task(coro: Any) -> None:
|
|
393
|
+
"""Schedule an async coroutine to run in the event loop from a sync context."""
|
|
394
|
+
if event_loop and event_loop.is_running():
|
|
395
|
+
asyncio.run_coroutine_threadsafe(coro, event_loop)
|
|
396
|
+
else:
|
|
397
|
+
logger.warning("Event loop not available for scheduling async task")
|
griptape_nodes/app/watch.py
CHANGED
|
@@ -30,7 +30,7 @@ class ReloadHandler(PatternMatchingEventHandler):
|
|
|
30
30
|
def start_process(self) -> None:
|
|
31
31
|
if self.process:
|
|
32
32
|
self.process.terminate()
|
|
33
|
-
self.process = subprocess.Popen(
|
|
33
|
+
self.process = subprocess.Popen(
|
|
34
34
|
["uv", "run", "gtn"], # noqa: S607
|
|
35
35
|
stdout=sys.stdout,
|
|
36
36
|
stderr=sys.stderr,
|
|
@@ -52,7 +52,9 @@ if __name__ == "__main__":
|
|
|
52
52
|
event_handler = ReloadHandler(patterns=["*.py"], ignore_patterns=["*.pyc", "*.pyo"], ignore_directories=True)
|
|
53
53
|
|
|
54
54
|
observer = Observer()
|
|
55
|
-
observer.schedule(event_handler, path="
|
|
55
|
+
observer.schedule(event_handler, path="src", recursive=True)
|
|
56
|
+
observer.schedule(event_handler, path="libraries", recursive=True)
|
|
57
|
+
observer.schedule(event_handler, path="tests", recursive=True)
|
|
56
58
|
observer.start()
|
|
57
59
|
|
|
58
60
|
try:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import threading
|
|
3
3
|
from multiprocessing import Process, Queue
|
|
4
|
+
from multiprocessing import Queue as ProcessQueue
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
|
-
from griptape_nodes.app.
|
|
8
|
+
from griptape_nodes.app.api import start_api
|
|
9
|
+
from griptape_nodes.app.app import _build_static_dir
|
|
8
10
|
from griptape_nodes.bootstrap.workflow_runners.local_workflow_runner import LocalWorkflowRunner
|
|
9
11
|
from griptape_nodes.bootstrap.workflow_runners.workflow_runner import WorkflowRunner
|
|
10
12
|
|
|
@@ -30,7 +32,9 @@ class SubprocessWorkflowRunner(WorkflowRunner):
|
|
|
30
32
|
logger.setLevel(logging.NOTSET)
|
|
31
33
|
|
|
32
34
|
try:
|
|
33
|
-
|
|
35
|
+
static_dir = _build_static_dir()
|
|
36
|
+
event_queue = ProcessQueue()
|
|
37
|
+
threading.Thread(target=start_api, args=(static_dir, event_queue), daemon=True).start()
|
|
34
38
|
workflow_runner = LocalWorkflowRunner(libraries)
|
|
35
39
|
workflow_runner.run(workflow_path, workflow_name, flow_input, "local")
|
|
36
40
|
except Exception as e:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -4,7 +4,8 @@ from urllib.parse import urljoin
|
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
|
|
7
|
-
from griptape_nodes.app.
|
|
7
|
+
from griptape_nodes.app.api import STATIC_SERVER_HOST, STATIC_SERVER_PORT, STATIC_SERVER_URL
|
|
8
|
+
from griptape_nodes.app.app import STATIC_SERVER_ENABLED
|
|
8
9
|
from griptape_nodes.drivers.storage.base_storage_driver import BaseStorageDriver, CreateSignedUploadUrlResponse
|
|
9
10
|
|
|
10
11
|
logger = logging.getLogger("griptape_nodes")
|
|
@@ -47,7 +48,8 @@ class LocalStorageDriver(BaseStorageDriver):
|
|
|
47
48
|
return {"url": url, "headers": response_data.get("headers", {}), "method": "PUT"}
|
|
48
49
|
|
|
49
50
|
def create_signed_download_url(self, file_name: str) -> str:
|
|
50
|
-
|
|
51
|
+
# The base_url already includes the /static path, so just append the filename
|
|
52
|
+
url = f"{self.base_url}/{file_name}"
|
|
51
53
|
# Add a cache-busting query parameter to the URL so that the browser always reloads the file
|
|
52
|
-
cache_busted_url =
|
|
54
|
+
cache_busted_url = f"{url}?t={int(time.time())}"
|
|
53
55
|
return cache_busted_url
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|