griptape-nodes 0.40.0__py3-none-any.whl → 0.42.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/app/__init__.py +1 -5
- griptape_nodes/app/app.py +12 -9
- griptape_nodes/app/app_sessions.py +132 -36
- griptape_nodes/app/watch.py +3 -1
- griptape_nodes/drivers/storage/local_storage_driver.py +3 -2
- griptape_nodes/exe_types/flow.py +68 -368
- griptape_nodes/machines/control_flow.py +16 -13
- griptape_nodes/machines/node_resolution.py +16 -14
- griptape_nodes/node_library/workflow_registry.py +2 -2
- griptape_nodes/retained_mode/events/agent_events.py +70 -8
- griptape_nodes/retained_mode/events/app_events.py +132 -11
- griptape_nodes/retained_mode/events/arbitrary_python_events.py +23 -0
- griptape_nodes/retained_mode/events/base_events.py +7 -25
- 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/library_events.py +193 -15
- griptape_nodes/retained_mode/events/logger_events.py +11 -0
- griptape_nodes/retained_mode/events/node_events.py +243 -22
- griptape_nodes/retained_mode/events/object_events.py +40 -4
- griptape_nodes/retained_mode/events/os_events.py +13 -2
- griptape_nodes/retained_mode/events/parameter_events.py +212 -8
- 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 +46 -323
- griptape_nodes/retained_mode/managers/agent_manager.py +1 -1
- 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 +749 -64
- griptape_nodes/retained_mode/managers/library_manager.py +112 -2
- griptape_nodes/retained_mode/managers/node_manager.py +35 -32
- griptape_nodes/retained_mode/managers/object_manager.py +11 -3
- griptape_nodes/retained_mode/managers/os_manager.py +70 -1
- 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/workflow_manager.py +523 -454
- griptape_nodes/retained_mode/retained_mode.py +44 -0
- griptape_nodes/retained_mode/utils/engine_identity.py +141 -27
- {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/METADATA +2 -2
- {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/RECORD +48 -47
- griptape_nodes/retained_mode/utils/session_persistence.py +0 -105
- {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/entry_points.txt +0 -0
- {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Manages session state and saving using XDG state directory.
|
|
2
|
+
|
|
3
|
+
Handles storing and retrieving multiple session information across engine restarts.
|
|
4
|
+
Sessions are tied to specific engines, with each engine maintaining its own session store.
|
|
5
|
+
Supports multiple concurrent sessions per engine with one active session managed through BaseEvent.
|
|
6
|
+
Storage structure: ~/.local/state/griptape_nodes/engines/{engine_id}/sessions.json
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from xdg_base_dirs import xdg_state_home
|
|
15
|
+
|
|
16
|
+
from griptape_nodes.retained_mode.events.base_events import BaseEvent
|
|
17
|
+
from griptape_nodes.retained_mode.managers.event_manager import EventManager
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("griptape_nodes")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SessionManager:
|
|
23
|
+
"""Manages session saving and active session state."""
|
|
24
|
+
|
|
25
|
+
_active_session_id: str | None = None
|
|
26
|
+
|
|
27
|
+
_SESSION_STATE_FILE = "sessions.json"
|
|
28
|
+
|
|
29
|
+
def __init__(self, event_manager: EventManager | None = None) -> None:
|
|
30
|
+
"""Initialize the SessionManager.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
event_manager: The EventManager instance to use for event handling.
|
|
34
|
+
"""
|
|
35
|
+
BaseEvent._session_id = self._active_session_id
|
|
36
|
+
if event_manager is not None:
|
|
37
|
+
# Register event handlers here when session events are defined
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def _get_session_state_dir(cls, engine_id: str | None = None) -> Path:
|
|
42
|
+
"""Get the XDG state directory for session storage.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
engine_id: Optional engine ID to create engine-specific directory
|
|
46
|
+
"""
|
|
47
|
+
base_dir = xdg_state_home() / "griptape_nodes"
|
|
48
|
+
if engine_id:
|
|
49
|
+
return base_dir / "engines" / engine_id
|
|
50
|
+
return base_dir
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def _get_session_state_file(cls, engine_id: str | None = None) -> Path:
|
|
54
|
+
"""Get the path to the session state storage file.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
engine_id: Optional engine ID to get engine-specific session file
|
|
58
|
+
"""
|
|
59
|
+
return cls._get_session_state_dir(engine_id) / cls._SESSION_STATE_FILE
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def _load_sessions_data(cls, engine_id: str | None = None) -> dict:
|
|
63
|
+
"""Load sessions data from storage.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
engine_id: Optional engine ID to load engine-specific sessions
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
dict: Sessions data structure with sessions array
|
|
70
|
+
"""
|
|
71
|
+
session_state_file = cls._get_session_state_file(engine_id)
|
|
72
|
+
|
|
73
|
+
if session_state_file.exists():
|
|
74
|
+
try:
|
|
75
|
+
with session_state_file.open("r") as f:
|
|
76
|
+
data = json.load(f)
|
|
77
|
+
if isinstance(data, dict) and "sessions" in data:
|
|
78
|
+
return {"sessions": data["sessions"]}
|
|
79
|
+
except (json.JSONDecodeError, OSError):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
return {"sessions": []}
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def _save_sessions_data(cls, sessions_data: dict, engine_id: str | None = None) -> None:
|
|
86
|
+
"""Save sessions data to storage.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
sessions_data: Sessions data structure to save
|
|
90
|
+
engine_id: Optional engine ID to save engine-specific sessions
|
|
91
|
+
"""
|
|
92
|
+
session_state_dir = cls._get_session_state_dir(engine_id)
|
|
93
|
+
session_state_dir.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
|
|
95
|
+
session_state_file = cls._get_session_state_file(engine_id)
|
|
96
|
+
with session_state_file.open("w") as f:
|
|
97
|
+
json.dump(sessions_data, f, indent=2)
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def _get_current_engine_id(cls) -> str | None:
|
|
101
|
+
"""Get the current engine ID from EngineIdentityManager.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
str | None: The current engine ID or None if not set
|
|
105
|
+
"""
|
|
106
|
+
# Import here to avoid circular imports
|
|
107
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
108
|
+
|
|
109
|
+
return GriptapeNodes.EngineIdentityManager().get_active_engine_id()
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def _find_session_by_id(cls, sessions_data: dict, session_id: str) -> dict | None:
|
|
113
|
+
"""Find a session by ID in the sessions data.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
sessions_data: The sessions data structure
|
|
117
|
+
session_id: The session ID to find
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
dict | None: The session data if found, None otherwise
|
|
121
|
+
"""
|
|
122
|
+
for session in sessions_data.get("sessions", []):
|
|
123
|
+
if session.get("session_id") == session_id:
|
|
124
|
+
return session
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def _add_or_update_session(cls, session_data: dict) -> None:
|
|
129
|
+
"""Add or update a session in the sessions data structure.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
session_data: The session data to add or update
|
|
133
|
+
"""
|
|
134
|
+
engine_id = cls._get_current_engine_id()
|
|
135
|
+
sessions_data = cls._load_sessions_data(engine_id)
|
|
136
|
+
|
|
137
|
+
# Find existing session
|
|
138
|
+
session_id = session_data["session_id"]
|
|
139
|
+
existing_session = cls._find_session_by_id(sessions_data, session_id)
|
|
140
|
+
|
|
141
|
+
if existing_session:
|
|
142
|
+
# Update existing session
|
|
143
|
+
existing_session.update(session_data)
|
|
144
|
+
existing_session["last_updated"] = datetime.now(tz=UTC).isoformat()
|
|
145
|
+
else:
|
|
146
|
+
# Add new session
|
|
147
|
+
session_data["engine_id"] = engine_id
|
|
148
|
+
sessions_data.setdefault("sessions", []).append(session_data)
|
|
149
|
+
|
|
150
|
+
cls._save_sessions_data(sessions_data, engine_id)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def get_active_session_id(cls) -> str | None:
|
|
154
|
+
"""Get the active session ID.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
str | None: The active session ID or None if not set
|
|
158
|
+
"""
|
|
159
|
+
return cls._active_session_id
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def set_active_session_id(cls, session_id: str) -> None:
|
|
163
|
+
"""Set the active session ID.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
session_id: The session ID to set as active
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
ValueError: If session_id is not found in persisted sessions
|
|
170
|
+
"""
|
|
171
|
+
engine_id = cls._get_current_engine_id()
|
|
172
|
+
sessions_data = cls._load_sessions_data(engine_id)
|
|
173
|
+
|
|
174
|
+
# Verify the session exists
|
|
175
|
+
if cls._find_session_by_id(sessions_data, session_id):
|
|
176
|
+
cls._active_session_id = session_id
|
|
177
|
+
logger.debug("Set active session ID to: %s", session_id)
|
|
178
|
+
else:
|
|
179
|
+
msg = f"Session with ID {session_id} not found for engine {engine_id}"
|
|
180
|
+
raise ValueError(msg)
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def save_session(cls, session_id: str) -> None:
|
|
184
|
+
"""Save a session and make it the active session.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
session_id: The session ID to save
|
|
188
|
+
"""
|
|
189
|
+
engine_id = cls._get_current_engine_id()
|
|
190
|
+
session_data = {
|
|
191
|
+
"session_id": session_id,
|
|
192
|
+
"engine_id": engine_id,
|
|
193
|
+
"started_at": datetime.now(tz=UTC).isoformat(),
|
|
194
|
+
"last_updated": datetime.now(tz=UTC).isoformat(),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Add or update the session
|
|
198
|
+
cls._add_or_update_session(session_data)
|
|
199
|
+
|
|
200
|
+
# Set as active session
|
|
201
|
+
cls._active_session_id = session_id
|
|
202
|
+
BaseEvent._session_id = session_id
|
|
203
|
+
logger.info("Saved and activated session: %s for engine: %s", session_id, engine_id)
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def get_saved_session_id(cls) -> str | None:
|
|
207
|
+
"""Get the active session ID if it exists.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
str | None: The active session ID or None if no active session
|
|
211
|
+
"""
|
|
212
|
+
# Return active session if set
|
|
213
|
+
if cls._active_session_id:
|
|
214
|
+
return cls._active_session_id
|
|
215
|
+
|
|
216
|
+
# Fall back to first session if available
|
|
217
|
+
engine_id = cls._get_current_engine_id()
|
|
218
|
+
sessions_data = cls._load_sessions_data(engine_id)
|
|
219
|
+
sessions = sessions_data.get("sessions", [])
|
|
220
|
+
if sessions:
|
|
221
|
+
first_session_id = sessions[0].get("session_id")
|
|
222
|
+
# Set as active for future calls
|
|
223
|
+
BaseEvent._session_id = first_session_id
|
|
224
|
+
cls._active_session_id = first_session_id
|
|
225
|
+
logger.debug("Retrieved first saved session as active: %s for engine: %s", first_session_id, engine_id)
|
|
226
|
+
return first_session_id
|
|
227
|
+
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
@classmethod
|
|
231
|
+
def clear_saved_session(cls) -> None:
|
|
232
|
+
"""Clear all saved session data for the current engine."""
|
|
233
|
+
# Clear active session
|
|
234
|
+
cls._active_session_id = None
|
|
235
|
+
BaseEvent._session_id = None
|
|
236
|
+
|
|
237
|
+
engine_id = cls._get_current_engine_id()
|
|
238
|
+
session_state_file = cls._get_session_state_file(engine_id)
|
|
239
|
+
if session_state_file.exists():
|
|
240
|
+
try:
|
|
241
|
+
session_state_file.unlink()
|
|
242
|
+
logger.info("Cleared all saved session data for engine: %s", engine_id)
|
|
243
|
+
except OSError:
|
|
244
|
+
# If we can't delete the file, just clear its contents
|
|
245
|
+
cls._save_sessions_data({"sessions": []}, engine_id)
|
|
246
|
+
logger.warning("Could not delete session file for engine %s, cleared contents instead", engine_id)
|
|
247
|
+
|
|
248
|
+
@classmethod
|
|
249
|
+
def has_saved_session(cls) -> bool:
|
|
250
|
+
"""Check if there is a saved session.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
bool: True if there is a saved session, False otherwise
|
|
254
|
+
"""
|
|
255
|
+
return cls.get_saved_session_id() is not None
|
|
256
|
+
|
|
257
|
+
@classmethod
|
|
258
|
+
def get_all_sessions(cls) -> list[dict]:
|
|
259
|
+
"""Get all registered sessions for the current engine.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
list[dict]: List of all session data for the current engine
|
|
263
|
+
"""
|
|
264
|
+
engine_id = cls._get_current_engine_id()
|
|
265
|
+
sessions_data = cls._load_sessions_data(engine_id)
|
|
266
|
+
return sessions_data.get("sessions", [])
|
|
267
|
+
|
|
268
|
+
@classmethod
|
|
269
|
+
def remove_session(cls, session_id: str) -> None:
|
|
270
|
+
"""Remove a session from the sessions data for the current engine.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
session_id: The session ID to remove
|
|
274
|
+
"""
|
|
275
|
+
engine_id = cls._get_current_engine_id()
|
|
276
|
+
sessions_data = cls._load_sessions_data(engine_id)
|
|
277
|
+
|
|
278
|
+
# Remove the session
|
|
279
|
+
sessions_data["sessions"] = [
|
|
280
|
+
session for session in sessions_data.get("sessions", []) if session.get("session_id") != session_id
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
# Clear active session if it was the removed session
|
|
284
|
+
if cls._active_session_id == session_id:
|
|
285
|
+
# Set to first remaining session or None
|
|
286
|
+
remaining_sessions = sessions_data.get("sessions", [])
|
|
287
|
+
cls._active_session_id = remaining_sessions[0].get("session_id") if remaining_sessions else None
|
|
288
|
+
logger.info(
|
|
289
|
+
"Removed active session %s for engine %s, set new active session to: %s",
|
|
290
|
+
session_id,
|
|
291
|
+
engine_id,
|
|
292
|
+
cls._active_session_id,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
cls._save_sessions_data(sessions_data, engine_id)
|
|
296
|
+
logger.info("Removed session: %s from engine: %s", session_id, engine_id)
|
|
297
|
+
|
|
298
|
+
@classmethod
|
|
299
|
+
def get_sessions_for_engine(cls, engine_id: str) -> list[dict]:
|
|
300
|
+
"""Get all sessions for a specific engine.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
engine_id: The engine ID to get sessions for
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
list[dict]: List of session data for the specified engine
|
|
307
|
+
"""
|
|
308
|
+
sessions_data = cls._load_sessions_data(engine_id)
|
|
309
|
+
return sessions_data.get("sessions", [])
|
|
310
|
+
|
|
311
|
+
@classmethod
|
|
312
|
+
def get_all_sessions_across_engines(cls) -> dict[str, list[dict]]:
|
|
313
|
+
"""Get all sessions across all engines.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
dict[str, list[dict]]: Dictionary mapping engine IDs to their session lists
|
|
317
|
+
"""
|
|
318
|
+
from griptape_nodes.retained_mode.utils.engine_identity import EngineIdentity
|
|
319
|
+
|
|
320
|
+
all_engines = EngineIdentity.get_all_engines()
|
|
321
|
+
result = {}
|
|
322
|
+
|
|
323
|
+
for engine in all_engines:
|
|
324
|
+
engine_id = engine.get("engine_id")
|
|
325
|
+
if engine_id:
|
|
326
|
+
result[engine_id] = cls.get_sessions_for_engine(engine_id)
|
|
327
|
+
|
|
328
|
+
return result
|
|
@@ -88,3 +88,10 @@ class Settings(BaseModel):
|
|
|
88
88
|
)
|
|
89
89
|
log_level: str = Field(default="INFO")
|
|
90
90
|
storage_backend: Literal["local", "gtc"] = Field(default="local")
|
|
91
|
+
minimum_disk_space_gb_libraries: float = Field(
|
|
92
|
+
default=10.0,
|
|
93
|
+
description="Minimum disk space in GB required for library installation and virtual environment operations",
|
|
94
|
+
)
|
|
95
|
+
minimum_disk_space_gb_workflows: float = Field(
|
|
96
|
+
default=1.0, description="Minimum disk space in GB required for saving workflows"
|
|
97
|
+
)
|