griptape-nodes 0.42.0__py3-none-any.whl → 0.43.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- griptape_nodes/__init__.py +0 -0
- griptape_nodes/app/.python-version +0 -0
- griptape_nodes/app/__init__.py +1 -6
- griptape_nodes/app/api.py +199 -0
- griptape_nodes/app/app.py +140 -225
- griptape_nodes/app/watch.py +1 -1
- 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 +2 -1
- 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 +0 -0
- 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 +41 -12
- griptape_nodes/machines/fsm.py +16 -2
- griptape_nodes/machines/node_resolution.py +0 -0
- 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 +1 -1
- 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 +0 -0
- griptape_nodes/retained_mode/events/app_events.py +6 -2
- griptape_nodes/retained_mode/events/arbitrary_python_events.py +0 -0
- griptape_nodes/retained_mode/events/base_events.py +6 -6
- griptape_nodes/retained_mode/events/config_events.py +0 -0
- griptape_nodes/retained_mode/events/connection_events.py +0 -0
- griptape_nodes/retained_mode/events/context_events.py +0 -0
- griptape_nodes/retained_mode/events/execution_events.py +0 -0
- griptape_nodes/retained_mode/events/flow_events.py +0 -0
- griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +0 -0
- griptape_nodes/retained_mode/events/library_events.py +2 -2
- griptape_nodes/retained_mode/events/logger_events.py +0 -0
- griptape_nodes/retained_mode/events/node_events.py +0 -0
- griptape_nodes/retained_mode/events/object_events.py +0 -0
- griptape_nodes/retained_mode/events/os_events.py +104 -2
- griptape_nodes/retained_mode/events/parameter_events.py +0 -0
- griptape_nodes/retained_mode/events/payload_registry.py +0 -0
- griptape_nodes/retained_mode/events/secrets_events.py +0 -0
- griptape_nodes/retained_mode/events/static_file_events.py +0 -0
- griptape_nodes/retained_mode/events/validation_events.py +0 -0
- griptape_nodes/retained_mode/events/workflow_events.py +0 -0
- griptape_nodes/retained_mode/griptape_nodes.py +43 -40
- griptape_nodes/retained_mode/managers/__init__.py +0 -0
- griptape_nodes/retained_mode/managers/agent_manager.py +48 -22
- 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 +0 -0
- griptape_nodes/retained_mode/managers/event_manager.py +0 -0
- griptape_nodes/retained_mode/managers/flow_manager.py +2 -0
- 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 +144 -39
- griptape_nodes/retained_mode/managers/node_manager.py +86 -72
- griptape_nodes/retained_mode/managers/object_manager.py +0 -0
- griptape_nodes/retained_mode/managers/operation_manager.py +0 -0
- griptape_nodes/retained_mode/managers/os_manager.py +517 -12
- griptape_nodes/retained_mode/managers/secrets_manager.py +0 -0
- griptape_nodes/retained_mode/managers/session_manager.py +0 -0
- griptape_nodes/retained_mode/managers/settings.py +0 -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 +199 -2
- griptape_nodes/retained_mode/retained_mode.py +0 -0
- griptape_nodes/retained_mode/utils/__init__.py +0 -0
- griptape_nodes/retained_mode/utils/engine_identity.py +0 -0
- 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.1.dist-info/METADATA +90 -0
- griptape_nodes-0.43.1.dist-info/RECORD +129 -0
- griptape_nodes-0.43.1.dist-info/WHEEL +4 -0
- {griptape_nodes-0.42.0.dist-info → griptape_nodes-0.43.1.dist-info}/entry_points.txt +1 -0
- griptape_nodes/app/app_sessions.py +0 -554
- griptape_nodes-0.42.0.dist-info/METADATA +0 -78
- griptape_nodes-0.42.0.dist-info/RECORD +0 -113
- griptape_nodes-0.42.0.dist-info/WHEEL +0 -4
- griptape_nodes-0.42.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,106 +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
|
-
)
|
|
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_path:path}")
|
|
153
|
-
async def create_static_file(request: Request, file_path: 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
|
-
file_full_path = Path(static_dir / file_path)
|
|
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)
|
|
160
124
|
|
|
161
|
-
|
|
162
|
-
file_full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
return api_key
|
|
163
126
|
|
|
164
|
-
data = await request.body()
|
|
165
|
-
try:
|
|
166
|
-
file_full_path.write_bytes(data)
|
|
167
|
-
except binascii.Error as e:
|
|
168
|
-
msg = f"Invalid base64 encoding for file {file_path}."
|
|
169
|
-
logger.error(msg)
|
|
170
|
-
raise HTTPException(status_code=400, detail=msg) from e
|
|
171
|
-
except (OSError, PermissionError) as e:
|
|
172
|
-
msg = f"Failed to write file {file_path} to {config_manager.workspace_path}: {e}"
|
|
173
|
-
logger.error(msg)
|
|
174
|
-
raise HTTPException(status_code=500, detail=msg) from e
|
|
175
|
-
|
|
176
|
-
static_url = f"http://{STATIC_SERVER_HOST}:{STATIC_SERVER_PORT}{STATIC_SERVER_URL}/{file_path}"
|
|
177
|
-
return {"url": static_url}
|
|
178
|
-
|
|
179
|
-
@app.post("/engines/request")
|
|
180
|
-
async def create_event(request: Request) -> None:
|
|
181
|
-
body = await request.json()
|
|
182
|
-
if "payload" in body:
|
|
183
|
-
__process_api_event(body["payload"])
|
|
184
|
-
|
|
185
|
-
logging.getLogger("uvicorn").addHandler(
|
|
186
|
-
RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True)
|
|
187
|
-
)
|
|
188
127
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
)
|
|
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"]
|
|
192
132
|
|
|
193
133
|
|
|
194
134
|
def _init_event_listeners() -> None:
|
|
@@ -219,38 +159,31 @@ def _init_event_listeners() -> None:
|
|
|
219
159
|
)
|
|
220
160
|
|
|
221
161
|
|
|
222
|
-
async def _alisten_for_api_requests() -> None:
|
|
162
|
+
async def _alisten_for_api_requests(api_key: str) -> None:
|
|
223
163
|
"""Listen for events from the Nodes API and process them asynchronously."""
|
|
224
164
|
global ws_connection_for_sending, event_loop # noqa: PLW0603
|
|
225
165
|
event_loop = asyncio.get_running_loop() # Store the event loop reference
|
|
226
|
-
nodes_app_url = os.getenv("GRIPTAPE_NODES_UI_BASE_URL", "https://nodes.griptape.ai")
|
|
227
166
|
logger.info("Listening for events from Nodes API via async WebSocket")
|
|
228
167
|
|
|
229
168
|
# Auto reconnect https://websockets.readthedocs.io/en/stable/reference/asyncio/client.html#opening-a-connection
|
|
230
|
-
connection_stream =
|
|
169
|
+
connection_stream = _create_websocket_connection(api_key)
|
|
231
170
|
initialized = False
|
|
232
171
|
async for ws_connection in connection_stream:
|
|
233
172
|
try:
|
|
234
173
|
ws_connection_for_sending = ws_connection # Store for sending events
|
|
174
|
+
ws_ready_event.set() # Signal that WebSocket is ready for sending
|
|
175
|
+
|
|
235
176
|
if not initialized:
|
|
236
|
-
|
|
177
|
+
event_queue.put(AppEvent(payload=app_events.AppInitializationComplete()))
|
|
237
178
|
initialized = True
|
|
238
179
|
|
|
180
|
+
event_queue.put(AppEvent(payload=app_events.AppConnectionEstablished()))
|
|
181
|
+
|
|
239
182
|
async for message in ws_connection:
|
|
240
183
|
try:
|
|
241
184
|
data = json.loads(message)
|
|
242
185
|
|
|
243
|
-
|
|
244
|
-
# With heartbeat events, we skip the regular processing and just send the heartbeat
|
|
245
|
-
# Technically no longer needed since https://github.com/griptape-ai/griptape-nodes/pull/369
|
|
246
|
-
# but we don't have a proper EventRequest for it yet.
|
|
247
|
-
if payload.get("request_type") == "Heartbeat":
|
|
248
|
-
session_id = GriptapeNodes.get_session_id()
|
|
249
|
-
await __send_heartbeat(
|
|
250
|
-
session_id=session_id, request=payload["request"], ws_connection=ws_connection
|
|
251
|
-
)
|
|
252
|
-
else:
|
|
253
|
-
__process_api_event(payload)
|
|
186
|
+
_process_api_event(data, event_queue)
|
|
254
187
|
except Exception:
|
|
255
188
|
logger.exception("Error processing event, skipping.")
|
|
256
189
|
except ConnectionClosed:
|
|
@@ -260,9 +193,9 @@ async def _alisten_for_api_requests() -> None:
|
|
|
260
193
|
await asyncio.sleep(2)
|
|
261
194
|
|
|
262
195
|
|
|
263
|
-
def _listen_for_api_events() -> None:
|
|
196
|
+
def _listen_for_api_events(api_key: str) -> None:
|
|
264
197
|
"""Run the async WebSocket listener in an event loop."""
|
|
265
|
-
asyncio.run(_alisten_for_api_requests())
|
|
198
|
+
asyncio.run(_alisten_for_api_requests(api_key))
|
|
266
199
|
|
|
267
200
|
|
|
268
201
|
def __process_node_event(event: GriptapeNodeEvent) -> None:
|
|
@@ -276,9 +209,8 @@ def __process_node_event(event: GriptapeNodeEvent) -> None:
|
|
|
276
209
|
else:
|
|
277
210
|
msg = f"Unknown/unsupported result event type encountered: '{type(result_event)}'."
|
|
278
211
|
raise TypeError(msg) from None
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
__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))
|
|
282
214
|
|
|
283
215
|
|
|
284
216
|
def __process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
|
|
@@ -286,7 +218,6 @@ def __process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
|
|
|
286
218
|
result_event = event.wrapped_event
|
|
287
219
|
if type(result_event.payload).__name__ == "NodeStartProcessEvent":
|
|
288
220
|
GriptapeNodes.EventManager().current_active_node = result_event.payload.node_name
|
|
289
|
-
event_json = result_event.json()
|
|
290
221
|
|
|
291
222
|
if type(result_event.payload).__name__ == "ResumeNodeProcessingEvent":
|
|
292
223
|
node_name = result_event.payload.node_name
|
|
@@ -300,7 +231,7 @@ def __process_execution_node_event(event: ExecutionGriptapeNodeEvent) -> None:
|
|
|
300
231
|
msg = "Node start and finish do not match."
|
|
301
232
|
raise KeyError(msg) from None
|
|
302
233
|
GriptapeNodes.EventManager().current_active_node = None
|
|
303
|
-
__schedule_async_task(__emit_message("execution_event",
|
|
234
|
+
__schedule_async_task(__emit_message("execution_event", result_event.json()))
|
|
304
235
|
|
|
305
236
|
|
|
306
237
|
def __process_progress_event(gt_event: ProgressEvent) -> None:
|
|
@@ -328,11 +259,16 @@ def _process_event_queue() -> None:
|
|
|
328
259
|
|
|
329
260
|
Event queue will be populated by background threads listening for events from the Nodes API.
|
|
330
261
|
"""
|
|
262
|
+
# Wait for WebSocket connection to be established before processing events
|
|
263
|
+
ws_ready_event.wait()
|
|
264
|
+
|
|
331
265
|
while True:
|
|
332
266
|
event = event_queue.get(block=True)
|
|
333
267
|
if isinstance(event, EventRequest):
|
|
334
268
|
request_payload = event.request
|
|
335
|
-
GriptapeNodes.handle_request(
|
|
269
|
+
GriptapeNodes.handle_request(
|
|
270
|
+
request_payload, response_topic=event.response_topic, request_id=event.request_id
|
|
271
|
+
)
|
|
336
272
|
elif isinstance(event, AppEvent):
|
|
337
273
|
__process_app_event(event)
|
|
338
274
|
else:
|
|
@@ -341,27 +277,11 @@ def _process_event_queue() -> None:
|
|
|
341
277
|
event_queue.task_done()
|
|
342
278
|
|
|
343
279
|
|
|
344
|
-
def
|
|
280
|
+
def _create_websocket_connection(api_key: str) -> Any:
|
|
345
281
|
"""Create an async WebSocket connection to the Nodes API."""
|
|
346
|
-
secrets_manager = GriptapeNodes.SecretsManager()
|
|
347
|
-
api_key = secrets_manager.get_secret("GT_CLOUD_API_KEY")
|
|
348
|
-
if api_key is None:
|
|
349
|
-
message = Panel(
|
|
350
|
-
Align.center(
|
|
351
|
-
"[bold red]Nodes API key is not set, please run [code]gtn init[/code] with a valid key: [/bold red]"
|
|
352
|
-
"[code]gtn init --api-key <your key>[/code]\n"
|
|
353
|
-
"[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]",
|
|
354
|
-
),
|
|
355
|
-
title="🔑 ❌ Missing Nodes API Key",
|
|
356
|
-
border_style="red",
|
|
357
|
-
padding=(1, 4),
|
|
358
|
-
)
|
|
359
|
-
console.print(message)
|
|
360
|
-
sys.exit(1)
|
|
361
|
-
|
|
362
282
|
endpoint = urljoin(
|
|
363
283
|
os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai").replace("http", "ws"),
|
|
364
|
-
"/ws/engines/events?
|
|
284
|
+
"/ws/engines/events?version=v2",
|
|
365
285
|
)
|
|
366
286
|
|
|
367
287
|
return connect(
|
|
@@ -370,7 +290,7 @@ def __create_async_websocket_connection() -> Any:
|
|
|
370
290
|
)
|
|
371
291
|
|
|
372
292
|
|
|
373
|
-
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:
|
|
374
294
|
"""Send a message via WebSocket asynchronously."""
|
|
375
295
|
global ws_connection_for_sending # noqa: PLW0602
|
|
376
296
|
if ws_connection_for_sending is None:
|
|
@@ -378,7 +298,12 @@ async def __emit_message(event_type: str, payload: str) -> None:
|
|
|
378
298
|
return
|
|
379
299
|
|
|
380
300
|
try:
|
|
381
|
-
|
|
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
|
+
|
|
382
307
|
await ws_connection_for_sending.send(json.dumps(body))
|
|
383
308
|
except WebSocketException as e:
|
|
384
309
|
logger.error("Error sending event to Nodes API: %s", e)
|
|
@@ -386,97 +311,87 @@ async def __emit_message(event_type: str, payload: str) -> None:
|
|
|
386
311
|
logger.error("Unexpected error while sending event to Nodes API: %s", e)
|
|
387
312
|
|
|
388
313
|
|
|
389
|
-
|
|
390
|
-
"""
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
"result": {},
|
|
394
|
-
"request_type": "Heartbeat",
|
|
395
|
-
"event_type": "EventResultSuccess",
|
|
396
|
-
"result_type": "HeartbeatSuccess",
|
|
397
|
-
**({"session_id": session_id} if session_id is not None else {}),
|
|
398
|
-
}
|
|
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()
|
|
399
318
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
"Responded to heartbeat request with session: %s and request: %s", session_id, request.get("request_id")
|
|
405
|
-
)
|
|
406
|
-
except WebSocketException as e:
|
|
407
|
-
logger.error("Error sending heartbeat response: %s", e)
|
|
408
|
-
except Exception as e:
|
|
409
|
-
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"
|
|
410
323
|
|
|
324
|
+
# Check for engine_id if no session_id
|
|
325
|
+
if engine_id:
|
|
326
|
+
return f"engines/{engine_id}/response"
|
|
411
327
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if event_loop and event_loop.is_running():
|
|
415
|
-
asyncio.run_coroutine_threadsafe(coro, event_loop)
|
|
416
|
-
else:
|
|
417
|
-
logger.warning("Event loop not available for scheduling async task")
|
|
328
|
+
# Default to generic response topic
|
|
329
|
+
return "response"
|
|
418
330
|
|
|
419
331
|
|
|
420
|
-
def
|
|
421
|
-
"""
|
|
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()
|
|
422
336
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
app_event = AppEvent(payload=payload)
|
|
428
|
-
event_queue.put(app_event)
|
|
429
|
-
|
|
430
|
-
engine_version_request = app_events.GetEngineVersionRequest()
|
|
431
|
-
engine_version_result = GriptapeNodes.get_instance().handle_engine_version_request(engine_version_request)
|
|
432
|
-
if isinstance(engine_version_result, app_events.GetEngineVersionResultSuccess):
|
|
433
|
-
engine_version = f"v{engine_version_result.major}.{engine_version_result.minor}.{engine_version_result.patch}"
|
|
434
|
-
else:
|
|
435
|
-
engine_version = "<UNKNOWN ENGINE VERSION>"
|
|
436
|
-
|
|
437
|
-
message = Panel(
|
|
438
|
-
Align.center(
|
|
439
|
-
f"[bold green]Engine is ready to receive events[/bold green]\n"
|
|
440
|
-
f"[bold blue]Return to: [link={nodes_app_url}]{nodes_app_url}[/link] to access the Workflow Editor[/bold blue]",
|
|
441
|
-
vertical="middle",
|
|
442
|
-
),
|
|
443
|
-
title="🚀 Griptape Nodes Engine Started",
|
|
444
|
-
subtitle=f"[green]{engine_version}[/green]",
|
|
445
|
-
border_style="green",
|
|
446
|
-
padding=(1, 4),
|
|
447
|
-
)
|
|
448
|
-
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"
|
|
449
341
|
|
|
342
|
+
# Check for engine_id if no session_id
|
|
343
|
+
if engine_id:
|
|
344
|
+
return f"engines/{engine_id}/request"
|
|
450
345
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
|
458
365
|
|
|
459
366
|
try:
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
msg = "Error: 'event_type' not found in request."
|
|
466
|
-
raise RuntimeError(msg) from None
|
|
467
|
-
|
|
468
|
-
# Now attempt to convert it into an EventRequest.
|
|
469
|
-
try:
|
|
470
|
-
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)
|
|
471
372
|
except Exception as e:
|
|
472
|
-
|
|
473
|
-
|
|
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
|
|
474
381
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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)
|
|
478
390
|
|
|
479
|
-
# Add the event to the queue
|
|
480
|
-
event_queue.put(request_event)
|
|
481
391
|
|
|
482
|
-
|
|
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,
|
|
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")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
griptape_nodes/exe_types/flow.py
CHANGED
|
File without changes
|