griptape-nodes 0.56.0__py3-none-any.whl → 0.56.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/app/app.py +10 -15
- griptape_nodes/app/watch.py +35 -67
- griptape_nodes/bootstrap/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/utils/python_subprocess_executor.py +122 -0
- griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +418 -0
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +37 -8
- griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +326 -0
- griptape_nodes/bootstrap/workflow_executors/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +51 -0
- griptape_nodes/bootstrap/workflow_publishers/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +43 -0
- griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +84 -0
- griptape_nodes/bootstrap/workflow_publishers/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +54 -0
- griptape_nodes/cli/commands/engine.py +4 -15
- griptape_nodes/cli/main.py +6 -1
- griptape_nodes/exe_types/core_types.py +26 -0
- griptape_nodes/exe_types/node_types.py +116 -1
- griptape_nodes/retained_mode/events/agent_events.py +2 -0
- griptape_nodes/retained_mode/events/base_events.py +18 -17
- griptape_nodes/retained_mode/events/execution_events.py +3 -1
- griptape_nodes/retained_mode/events/flow_events.py +5 -7
- griptape_nodes/retained_mode/events/mcp_events.py +363 -0
- griptape_nodes/retained_mode/events/node_events.py +3 -4
- griptape_nodes/retained_mode/griptape_nodes.py +8 -0
- griptape_nodes/retained_mode/managers/agent_manager.py +67 -4
- griptape_nodes/retained_mode/managers/event_manager.py +31 -13
- griptape_nodes/retained_mode/managers/flow_manager.py +76 -44
- griptape_nodes/retained_mode/managers/library_manager.py +7 -9
- griptape_nodes/retained_mode/managers/mcp_manager.py +364 -0
- griptape_nodes/retained_mode/managers/node_manager.py +12 -1
- griptape_nodes/retained_mode/managers/settings.py +40 -0
- griptape_nodes/retained_mode/managers/workflow_manager.py +94 -8
- griptape_nodes/traits/multi_options.py +5 -1
- griptape_nodes/traits/options.py +10 -2
- {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.56.1.dist-info}/METADATA +2 -2
- {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.56.1.dist-info}/RECORD +39 -26
- {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.56.1.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.56.0.dist-info → griptape_nodes-0.56.1.dist-info}/entry_points.txt +0 -0
griptape_nodes/app/app.py
CHANGED
|
@@ -125,13 +125,10 @@ async def astart_app() -> None:
|
|
|
125
125
|
# Initialize event queue in main thread
|
|
126
126
|
griptape_nodes.EventManager().initialize_queue()
|
|
127
127
|
|
|
128
|
-
# Get main loop reference
|
|
129
|
-
main_loop = asyncio.get_running_loop()
|
|
130
|
-
|
|
131
128
|
try:
|
|
132
129
|
# Start WebSocket tasks in daemon thread
|
|
133
130
|
threading.Thread(
|
|
134
|
-
target=_start_websocket_connection, args=(api_key,
|
|
131
|
+
target=_start_websocket_connection, args=(api_key,), daemon=True, name="websocket-tasks"
|
|
135
132
|
).start()
|
|
136
133
|
|
|
137
134
|
# Run event processing on main thread
|
|
@@ -142,7 +139,7 @@ async def astart_app() -> None:
|
|
|
142
139
|
raise
|
|
143
140
|
|
|
144
141
|
|
|
145
|
-
def _start_websocket_connection(api_key: str
|
|
142
|
+
def _start_websocket_connection(api_key: str) -> None:
|
|
146
143
|
"""Run WebSocket tasks in a separate thread with its own async loop."""
|
|
147
144
|
global websocket_event_loop # noqa: PLW0603
|
|
148
145
|
try:
|
|
@@ -155,7 +152,7 @@ def _start_websocket_connection(api_key: str, main_loop: asyncio.AbstractEventLo
|
|
|
155
152
|
websocket_event_loop_ready.set()
|
|
156
153
|
|
|
157
154
|
# Run the async WebSocket tasks
|
|
158
|
-
loop.run_until_complete(_run_websocket_tasks(api_key
|
|
155
|
+
loop.run_until_complete(_run_websocket_tasks(api_key))
|
|
159
156
|
except Exception as e:
|
|
160
157
|
logger.error("WebSocket thread error: %s", e)
|
|
161
158
|
raise
|
|
@@ -164,7 +161,7 @@ def _start_websocket_connection(api_key: str, main_loop: asyncio.AbstractEventLo
|
|
|
164
161
|
websocket_event_loop_ready.clear()
|
|
165
162
|
|
|
166
163
|
|
|
167
|
-
async def _run_websocket_tasks(api_key: str
|
|
164
|
+
async def _run_websocket_tasks(api_key: str) -> None:
|
|
168
165
|
"""Run WebSocket tasks - async version."""
|
|
169
166
|
# Create WebSocket connection for this thread
|
|
170
167
|
connection_stream = _create_websocket_connection(api_key)
|
|
@@ -181,12 +178,10 @@ async def _run_websocket_tasks(api_key: str, main_loop: asyncio.AbstractEventLoo
|
|
|
181
178
|
initialized = True
|
|
182
179
|
|
|
183
180
|
# Emit connection established event for every connection
|
|
184
|
-
griptape_nodes.EventManager().
|
|
185
|
-
main_loop, AppEvent(payload=app_events.AppConnectionEstablished())
|
|
186
|
-
)
|
|
181
|
+
griptape_nodes.EventManager().put_event(AppEvent(payload=app_events.AppConnectionEstablished()))
|
|
187
182
|
|
|
188
183
|
async with asyncio.TaskGroup() as tg:
|
|
189
|
-
tg.create_task(_process_incoming_messages(ws_connection
|
|
184
|
+
tg.create_task(_process_incoming_messages(ws_connection))
|
|
190
185
|
tg.create_task(_send_outgoing_messages(ws_connection))
|
|
191
186
|
except (ExceptionGroup, ConnectionClosed, ConnectionClosedError):
|
|
192
187
|
logger.info("WebSocket connection closed, reconnecting...")
|
|
@@ -217,14 +212,14 @@ def _ensure_api_key() -> str:
|
|
|
217
212
|
return api_key
|
|
218
213
|
|
|
219
214
|
|
|
220
|
-
async def _process_incoming_messages(ws_connection: Any
|
|
215
|
+
async def _process_incoming_messages(ws_connection: Any) -> None:
|
|
221
216
|
"""Process incoming WebSocket requests from Nodes API."""
|
|
222
217
|
logger.debug("Processing incoming WebSocket requests from WebSocket connection")
|
|
223
218
|
|
|
224
219
|
async for message in ws_connection:
|
|
225
220
|
try:
|
|
226
221
|
data = json.loads(message)
|
|
227
|
-
await _process_api_event(data
|
|
222
|
+
await _process_api_event(data)
|
|
228
223
|
except Exception:
|
|
229
224
|
logger.exception("Error processing event, skipping.")
|
|
230
225
|
|
|
@@ -242,7 +237,7 @@ def _create_websocket_connection(api_key: str) -> Any:
|
|
|
242
237
|
)
|
|
243
238
|
|
|
244
239
|
|
|
245
|
-
async def _process_api_event(event: dict
|
|
240
|
+
async def _process_api_event(event: dict) -> None:
|
|
246
241
|
"""Process API events and add to async queue."""
|
|
247
242
|
payload = event.get("payload", {})
|
|
248
243
|
|
|
@@ -278,7 +273,7 @@ async def _process_api_event(event: dict, main_loop: asyncio.AbstractEventLoop)
|
|
|
278
273
|
await _process_event_request(request_event)
|
|
279
274
|
else:
|
|
280
275
|
# Add the event to the main thread event queue for processing
|
|
281
|
-
griptape_nodes.EventManager().
|
|
276
|
+
griptape_nodes.EventManager().put_event(request_event)
|
|
282
277
|
|
|
283
278
|
|
|
284
279
|
async def _send_outgoing_messages(ws_connection: Any) -> None:
|
griptape_nodes/app/watch.py
CHANGED
|
@@ -2,85 +2,53 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import subprocess
|
|
4
4
|
import sys
|
|
5
|
-
import time
|
|
6
|
-
from typing import Any
|
|
7
5
|
|
|
8
|
-
from
|
|
9
|
-
from watchdog.observers import Observer
|
|
6
|
+
from watchfiles import DefaultFilter, watch
|
|
10
7
|
|
|
11
8
|
from griptape_nodes.utils.uv_utils import find_uv_bin
|
|
12
9
|
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
) -> None:
|
|
23
|
-
super().__init__(
|
|
24
|
-
patterns=patterns,
|
|
25
|
-
ignore_patterns=ignore_patterns,
|
|
26
|
-
ignore_directories=ignore_directories,
|
|
27
|
-
case_sensitive=case_sensitive,
|
|
28
|
-
)
|
|
29
|
-
self.process = None
|
|
30
|
-
self.start_process()
|
|
11
|
+
def start_process() -> subprocess.Popen:
|
|
12
|
+
"""Start the gtn process."""
|
|
13
|
+
uv_path = find_uv_bin()
|
|
14
|
+
return subprocess.Popen( # noqa: S603
|
|
15
|
+
[uv_path, "run", "gtn"],
|
|
16
|
+
stdout=sys.stdout,
|
|
17
|
+
stderr=sys.stderr,
|
|
18
|
+
)
|
|
31
19
|
|
|
32
|
-
def start_process(self) -> None:
|
|
33
|
-
if self.process:
|
|
34
|
-
self._terminate_process(self.process)
|
|
35
|
-
uv_path = find_uv_bin()
|
|
36
|
-
self.process = subprocess.Popen( # noqa: S603
|
|
37
|
-
[uv_path, "run", "gtn"],
|
|
38
|
-
stdout=sys.stdout,
|
|
39
|
-
stderr=sys.stderr,
|
|
40
|
-
)
|
|
41
20
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
21
|
+
def terminate_process(process: subprocess.Popen) -> None:
|
|
22
|
+
"""Gracefully terminate a process with timeout."""
|
|
23
|
+
if process.poll() is not None:
|
|
24
|
+
return # Process already terminated
|
|
46
25
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def on_modified(self, event: Any) -> None:
|
|
58
|
-
"""Called on any file event in the watched directory (create, modify, delete, move)."""
|
|
59
|
-
# Don't reload if the event is on a directory
|
|
60
|
-
if event.is_directory:
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
if str(event.src_path).endswith(__file__):
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
self.start_process()
|
|
26
|
+
# First try graceful termination
|
|
27
|
+
process.terminate()
|
|
28
|
+
try:
|
|
29
|
+
# Wait up to 5 seconds for graceful shutdown
|
|
30
|
+
process.wait(timeout=5)
|
|
31
|
+
except subprocess.TimeoutExpired:
|
|
32
|
+
# Force kill if it doesn't shut down gracefully
|
|
33
|
+
process.kill()
|
|
34
|
+
process.wait()
|
|
67
35
|
|
|
68
36
|
|
|
69
37
|
if __name__ == "__main__":
|
|
70
|
-
|
|
38
|
+
# Configure filter to ignore .venv, __pycache__, and compiled Python files
|
|
39
|
+
watch_filter = DefaultFilter(
|
|
40
|
+
ignore_dirs=(".venv",),
|
|
41
|
+
ignore_entity_patterns=(r"\.pyc$", r"\.pyo$"),
|
|
42
|
+
)
|
|
71
43
|
|
|
72
|
-
|
|
73
|
-
observer.schedule(event_handler, path="src", recursive=True)
|
|
74
|
-
observer.schedule(event_handler, path="libraries", recursive=True)
|
|
75
|
-
observer.schedule(event_handler, path="tests", recursive=True)
|
|
76
|
-
observer.start()
|
|
44
|
+
process = start_process()
|
|
77
45
|
|
|
78
46
|
try:
|
|
79
|
-
|
|
80
|
-
|
|
47
|
+
# Watch for changes in src, libraries, and tests directories
|
|
48
|
+
for changes in watch("src", "libraries", "tests", watch_filter=watch_filter):
|
|
49
|
+
# Only restart on .py file changes
|
|
50
|
+
if any(str(path).endswith(".py") for _, path in changes):
|
|
51
|
+
terminate_process(process)
|
|
52
|
+
process = start_process()
|
|
81
53
|
except KeyboardInterrupt:
|
|
82
|
-
|
|
83
|
-
event_handler._terminate_process(event_handler.process)
|
|
84
|
-
finally:
|
|
85
|
-
observer.stop()
|
|
86
|
-
observer.join()
|
|
54
|
+
terminate_process(process)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Bootstrap utils package."""
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PythonSubprocessExecutorError(Exception):
|
|
15
|
+
"""Exception raised during Python subprocess execution."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PythonSubprocessExecutor:
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self._process: asyncio.subprocess.Process | None = None
|
|
21
|
+
self._is_running = False
|
|
22
|
+
|
|
23
|
+
async def execute_python_script(
|
|
24
|
+
self, script_path: Path, args: list[str] | None = None, cwd: Path | None = None
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Execute a Python script in a subprocess and wait for completion.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
script_path: Path to the Python script to execute
|
|
30
|
+
args: Additional command line arguments
|
|
31
|
+
cwd: Working directory for the subprocess
|
|
32
|
+
"""
|
|
33
|
+
if self.is_running():
|
|
34
|
+
logger.warning("Another subprocess is already running. Terminating it first.")
|
|
35
|
+
await self.terminate()
|
|
36
|
+
|
|
37
|
+
args = args or []
|
|
38
|
+
command = [sys.executable, str(script_path), *args]
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
logger.info("Starting subprocess: %s", " ".join(command))
|
|
42
|
+
logger.info("Working directory: %s", cwd)
|
|
43
|
+
|
|
44
|
+
self._process = await asyncio.create_subprocess_exec(
|
|
45
|
+
*command,
|
|
46
|
+
cwd=cwd,
|
|
47
|
+
stdout=asyncio.subprocess.PIPE,
|
|
48
|
+
stderr=asyncio.subprocess.PIPE,
|
|
49
|
+
)
|
|
50
|
+
self._is_running = True
|
|
51
|
+
logger.info("Subprocess started with PID: %s", self._process.pid)
|
|
52
|
+
|
|
53
|
+
stdout_bytes, stderr_bytes = await self._process.communicate()
|
|
54
|
+
returncode = self._process.returncode
|
|
55
|
+
stdout = stdout_bytes.decode() if stdout_bytes else ""
|
|
56
|
+
stderr = stderr_bytes.decode() if stderr_bytes else ""
|
|
57
|
+
|
|
58
|
+
# Log all output regardless of return code
|
|
59
|
+
if stdout:
|
|
60
|
+
logger.info("Subprocess stdout: %s", stdout)
|
|
61
|
+
if stderr:
|
|
62
|
+
logger.info("Subprocess stderr: %s", stderr)
|
|
63
|
+
|
|
64
|
+
if returncode == 0:
|
|
65
|
+
logger.info("Subprocess completed successfully with return code: %d", returncode)
|
|
66
|
+
else:
|
|
67
|
+
logger.error("Subprocess failed with return code: %d", returncode)
|
|
68
|
+
msg = f"Subprocess failed with return code: {returncode}"
|
|
69
|
+
raise RuntimeError(msg) # noqa: TRY301
|
|
70
|
+
|
|
71
|
+
except Exception as e:
|
|
72
|
+
msg = "Error running subprocess"
|
|
73
|
+
logger.exception(msg)
|
|
74
|
+
raise PythonSubprocessExecutorError(msg) from e
|
|
75
|
+
finally:
|
|
76
|
+
self._is_running = False
|
|
77
|
+
self._process = None
|
|
78
|
+
|
|
79
|
+
def is_running(self) -> bool:
|
|
80
|
+
"""Check if a subprocess is currently running."""
|
|
81
|
+
return self._is_running
|
|
82
|
+
|
|
83
|
+
async def terminate(self) -> bool:
|
|
84
|
+
"""Terminate the running subprocess.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if successfully terminated, False otherwise
|
|
88
|
+
"""
|
|
89
|
+
if not self.is_running() or not self._process:
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
logger.info("Terminating subprocess...")
|
|
94
|
+
self._process.terminate()
|
|
95
|
+
|
|
96
|
+
# Wait for graceful termination with timeout using context manager
|
|
97
|
+
try:
|
|
98
|
+
async with asyncio.timeout(5.0):
|
|
99
|
+
await self._process.wait()
|
|
100
|
+
logger.info("Subprocess terminated gracefully")
|
|
101
|
+
return True # noqa: TRY300
|
|
102
|
+
except TimeoutError:
|
|
103
|
+
logger.warning("Subprocess did not terminate gracefully, force killing...")
|
|
104
|
+
self._process.kill()
|
|
105
|
+
await self._process.wait()
|
|
106
|
+
logger.info("Subprocess force killed")
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error("Error terminating subprocess: %s", e)
|
|
111
|
+
return False
|
|
112
|
+
finally:
|
|
113
|
+
self._is_running = False
|
|
114
|
+
self._process = None
|
|
115
|
+
|
|
116
|
+
def get_status(self) -> dict[str, Any]:
|
|
117
|
+
"""Get current status information."""
|
|
118
|
+
return {
|
|
119
|
+
"is_running": self.is_running(),
|
|
120
|
+
"has_process": self._process is not None,
|
|
121
|
+
"process_pid": self._process.pid if self._process else None,
|
|
122
|
+
}
|