griptape-nodes 0.41.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.
Files changed (49) hide show
  1. griptape_nodes/app/__init__.py +1 -5
  2. griptape_nodes/app/app.py +12 -9
  3. griptape_nodes/app/app_sessions.py +132 -36
  4. griptape_nodes/app/watch.py +3 -1
  5. griptape_nodes/drivers/storage/local_storage_driver.py +3 -2
  6. griptape_nodes/exe_types/flow.py +68 -368
  7. griptape_nodes/machines/control_flow.py +16 -13
  8. griptape_nodes/machines/node_resolution.py +16 -14
  9. griptape_nodes/node_library/workflow_registry.py +2 -2
  10. griptape_nodes/retained_mode/events/agent_events.py +70 -8
  11. griptape_nodes/retained_mode/events/app_events.py +132 -11
  12. griptape_nodes/retained_mode/events/arbitrary_python_events.py +23 -0
  13. griptape_nodes/retained_mode/events/base_events.py +7 -25
  14. griptape_nodes/retained_mode/events/config_events.py +87 -11
  15. griptape_nodes/retained_mode/events/connection_events.py +56 -5
  16. griptape_nodes/retained_mode/events/context_events.py +27 -4
  17. griptape_nodes/retained_mode/events/execution_events.py +99 -14
  18. griptape_nodes/retained_mode/events/flow_events.py +165 -7
  19. griptape_nodes/retained_mode/events/library_events.py +193 -15
  20. griptape_nodes/retained_mode/events/logger_events.py +11 -0
  21. griptape_nodes/retained_mode/events/node_events.py +242 -22
  22. griptape_nodes/retained_mode/events/object_events.py +40 -4
  23. griptape_nodes/retained_mode/events/os_events.py +13 -2
  24. griptape_nodes/retained_mode/events/parameter_events.py +212 -8
  25. griptape_nodes/retained_mode/events/secrets_events.py +59 -7
  26. griptape_nodes/retained_mode/events/static_file_events.py +57 -4
  27. griptape_nodes/retained_mode/events/validation_events.py +39 -4
  28. griptape_nodes/retained_mode/events/workflow_events.py +188 -17
  29. griptape_nodes/retained_mode/griptape_nodes.py +46 -323
  30. griptape_nodes/retained_mode/managers/agent_manager.py +1 -1
  31. griptape_nodes/retained_mode/managers/engine_identity_manager.py +146 -0
  32. griptape_nodes/retained_mode/managers/event_manager.py +14 -2
  33. griptape_nodes/retained_mode/managers/flow_manager.py +749 -64
  34. griptape_nodes/retained_mode/managers/library_manager.py +112 -2
  35. griptape_nodes/retained_mode/managers/node_manager.py +34 -31
  36. griptape_nodes/retained_mode/managers/object_manager.py +11 -3
  37. griptape_nodes/retained_mode/managers/os_manager.py +70 -1
  38. griptape_nodes/retained_mode/managers/secrets_manager.py +4 -0
  39. griptape_nodes/retained_mode/managers/session_manager.py +328 -0
  40. griptape_nodes/retained_mode/managers/settings.py +7 -0
  41. griptape_nodes/retained_mode/managers/workflow_manager.py +523 -454
  42. griptape_nodes/retained_mode/retained_mode.py +44 -0
  43. griptape_nodes/retained_mode/utils/engine_identity.py +141 -27
  44. {griptape_nodes-0.41.0.dist-info → griptape_nodes-0.42.0.dist-info}/METADATA +2 -2
  45. {griptape_nodes-0.41.0.dist-info → griptape_nodes-0.42.0.dist-info}/RECORD +48 -47
  46. griptape_nodes/retained_mode/utils/session_persistence.py +0 -105
  47. {griptape_nodes-0.41.0.dist-info → griptape_nodes-0.42.0.dist-info}/WHEEL +0 -0
  48. {griptape_nodes-0.41.0.dist-info → griptape_nodes-0.42.0.dist-info}/entry_points.txt +0 -0
  49. {griptape_nodes-0.41.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
+ )