griptape-nodes 0.42.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 -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.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.42.0.dist-info → griptape_nodes-0.43.0.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
|
@@ -1,27 +1,38 @@
|
|
|
1
|
-
import
|
|
1
|
+
import base64
|
|
2
|
+
import mimetypes
|
|
2
3
|
import os
|
|
3
4
|
import shutil
|
|
4
5
|
import subprocess
|
|
5
6
|
import sys
|
|
7
|
+
from dataclasses import dataclass
|
|
6
8
|
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
9
|
+
from typing import Any
|
|
8
10
|
|
|
11
|
+
from binaryornot.check import is_binary
|
|
9
12
|
from rich.console import Console
|
|
10
13
|
|
|
11
14
|
from griptape_nodes.retained_mode.events.base_events import ResultPayload
|
|
12
15
|
from griptape_nodes.retained_mode.events.os_events import (
|
|
16
|
+
FileSystemEntry,
|
|
17
|
+
ListDirectoryRequest,
|
|
18
|
+
ListDirectoryResultFailure,
|
|
19
|
+
ListDirectoryResultSuccess,
|
|
13
20
|
OpenAssociatedFileRequest,
|
|
14
21
|
OpenAssociatedFileResultFailure,
|
|
15
22
|
OpenAssociatedFileResultSuccess,
|
|
23
|
+
ReadFileRequest,
|
|
24
|
+
ReadFileResultFailure,
|
|
25
|
+
ReadFileResultSuccess,
|
|
16
26
|
)
|
|
27
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes, logger
|
|
17
28
|
from griptape_nodes.retained_mode.managers.event_manager import EventManager
|
|
18
29
|
|
|
19
30
|
console = Console()
|
|
20
|
-
logger = logging.getLogger("griptape_nodes")
|
|
21
31
|
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
@dataclass
|
|
34
|
+
class DiskSpaceInfo:
|
|
35
|
+
"""Information about disk space usage."""
|
|
25
36
|
|
|
26
37
|
total: int
|
|
27
38
|
used: int
|
|
@@ -40,6 +51,127 @@ class OSManager:
|
|
|
40
51
|
event_manager.assign_manager_to_request_type(
|
|
41
52
|
request_type=OpenAssociatedFileRequest, callback=self.on_open_associated_file_request
|
|
42
53
|
)
|
|
54
|
+
event_manager.assign_manager_to_request_type(
|
|
55
|
+
request_type=ListDirectoryRequest, callback=self.on_list_directory_request
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
event_manager.assign_manager_to_request_type(
|
|
59
|
+
request_type=ReadFileRequest, callback=self.on_read_file_request
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def _get_workspace_path(self) -> Path:
|
|
63
|
+
"""Get the workspace path from config."""
|
|
64
|
+
return GriptapeNodes.ConfigManager().workspace_path
|
|
65
|
+
|
|
66
|
+
def _expand_path(self, path_str: str) -> Path:
|
|
67
|
+
"""Expand a path string, handling tilde and environment variables.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
path_str: Path string that may contain ~ or environment variables
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Expanded Path object
|
|
74
|
+
"""
|
|
75
|
+
# Expand environment variables first, then tilde
|
|
76
|
+
expanded_vars = os.path.expandvars(path_str)
|
|
77
|
+
return Path(expanded_vars).expanduser().resolve()
|
|
78
|
+
|
|
79
|
+
def _resolve_file_path(self, path_str: str, *, workspace_only: bool = False) -> Path:
|
|
80
|
+
"""Resolve a file path, handling absolute, relative, and tilde paths.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
path_str: Path string that may be absolute, relative, or start with ~
|
|
84
|
+
workspace_only: If True and path is invalid, fall back to workspace directory
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Resolved Path object
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
if Path(path_str).is_absolute() or path_str.startswith("~"):
|
|
91
|
+
# Expand tilde and environment variables for absolute paths or paths starting with ~
|
|
92
|
+
return self._expand_path(path_str)
|
|
93
|
+
# Both workspace and system-wide modes resolve relative to current directory
|
|
94
|
+
return (self._get_workspace_path() / path_str).resolve()
|
|
95
|
+
except (ValueError, RuntimeError):
|
|
96
|
+
if workspace_only:
|
|
97
|
+
msg = f"Path '{path_str}' not found, using workspace directory: {self._get_workspace_path()}"
|
|
98
|
+
logger.warning(msg)
|
|
99
|
+
return self._get_workspace_path()
|
|
100
|
+
# Re-raise the exception for non-workspace mode
|
|
101
|
+
raise
|
|
102
|
+
|
|
103
|
+
def _validate_workspace_path(self, path: Path) -> tuple[bool, Path]:
|
|
104
|
+
"""Check if a path is within workspace and return relative path if it is.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
path: Path to validate
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Tuple of (is_workspace_path, relative_or_absolute_path)
|
|
111
|
+
"""
|
|
112
|
+
workspace = GriptapeNodes.ConfigManager().workspace_path
|
|
113
|
+
|
|
114
|
+
# Ensure both paths are resolved for comparison
|
|
115
|
+
path = path.resolve()
|
|
116
|
+
workspace = workspace.resolve()
|
|
117
|
+
|
|
118
|
+
msg = f"Validating path: {path} against workspace: {workspace}"
|
|
119
|
+
logger.debug(msg)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
relative = path.relative_to(workspace)
|
|
123
|
+
except ValueError:
|
|
124
|
+
msg = f"Path is outside workspace: {path}"
|
|
125
|
+
logger.debug(msg)
|
|
126
|
+
return False, path
|
|
127
|
+
|
|
128
|
+
msg = f"Path is within workspace, relative path: {relative}"
|
|
129
|
+
logger.debug(msg)
|
|
130
|
+
return True, relative
|
|
131
|
+
|
|
132
|
+
def _validate_read_file_request(self, request: ReadFileRequest) -> tuple[Path, str]:
|
|
133
|
+
"""Validate read file request and return resolved file path and path string."""
|
|
134
|
+
# Validate that exactly one of file_path or file_entry is provided
|
|
135
|
+
if request.file_path is None and request.file_entry is None:
|
|
136
|
+
msg = "Either file_path or file_entry must be provided"
|
|
137
|
+
logger.error(msg)
|
|
138
|
+
raise ValueError(msg)
|
|
139
|
+
|
|
140
|
+
if request.file_path is not None and request.file_entry is not None:
|
|
141
|
+
msg = "Only one of file_path or file_entry should be provided, not both"
|
|
142
|
+
logger.error(msg)
|
|
143
|
+
raise ValueError(msg)
|
|
144
|
+
|
|
145
|
+
# Get the file path to read - handle paths consistently
|
|
146
|
+
if request.file_entry is not None:
|
|
147
|
+
file_path_str = request.file_entry.path
|
|
148
|
+
elif request.file_path is not None:
|
|
149
|
+
file_path_str = request.file_path
|
|
150
|
+
else:
|
|
151
|
+
msg = "No valid file path provided"
|
|
152
|
+
logger.error(msg)
|
|
153
|
+
raise ValueError(msg)
|
|
154
|
+
|
|
155
|
+
file_path = self._resolve_file_path(file_path_str, workspace_only=request.workspace_only is True)
|
|
156
|
+
|
|
157
|
+
# Check if file exists and is actually a file
|
|
158
|
+
if not file_path.exists():
|
|
159
|
+
msg = f"File does not exist: {file_path}"
|
|
160
|
+
logger.error(msg)
|
|
161
|
+
raise FileNotFoundError(msg)
|
|
162
|
+
if not file_path.is_file():
|
|
163
|
+
msg = f"File is not a file: {file_path}"
|
|
164
|
+
logger.error(msg)
|
|
165
|
+
raise FileNotFoundError(msg)
|
|
166
|
+
|
|
167
|
+
# Check workspace constraints
|
|
168
|
+
is_workspace_path, _ = self._validate_workspace_path(file_path)
|
|
169
|
+
if request.workspace_only and not is_workspace_path:
|
|
170
|
+
msg = f"File is outside workspace: {file_path}"
|
|
171
|
+
logger.error(msg)
|
|
172
|
+
raise ValueError(msg)
|
|
173
|
+
|
|
174
|
+
return file_path, file_path_str
|
|
43
175
|
|
|
44
176
|
@staticmethod
|
|
45
177
|
def platform() -> str:
|
|
@@ -72,12 +204,43 @@ class OSManager:
|
|
|
72
204
|
sys.stdout.flush() # Recommended here https://docs.python.org/3/library/os.html#os.execvpe
|
|
73
205
|
os.execvp(args[0], args) # noqa: S606
|
|
74
206
|
|
|
75
|
-
def on_open_associated_file_request(self, request: OpenAssociatedFileRequest) -> ResultPayload: # noqa: PLR0911
|
|
207
|
+
def on_open_associated_file_request(self, request: OpenAssociatedFileRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912
|
|
208
|
+
# Validate that exactly one of path_to_file or file_entry is provided
|
|
209
|
+
if request.path_to_file is None and request.file_entry is None:
|
|
210
|
+
msg = "Either path_to_file or file_entry must be provided"
|
|
211
|
+
logger.error(msg)
|
|
212
|
+
return OpenAssociatedFileResultFailure()
|
|
213
|
+
|
|
214
|
+
if request.path_to_file is not None and request.file_entry is not None:
|
|
215
|
+
msg = "Only one of path_to_file or file_entry should be provided, not both"
|
|
216
|
+
logger.error(msg)
|
|
217
|
+
return OpenAssociatedFileResultFailure()
|
|
218
|
+
|
|
219
|
+
# Get the file path to open
|
|
220
|
+
if request.file_entry is not None:
|
|
221
|
+
# Use the path from the FileSystemEntry
|
|
222
|
+
file_path_str = request.file_entry.path
|
|
223
|
+
elif request.path_to_file is not None:
|
|
224
|
+
# Use the provided path_to_file
|
|
225
|
+
file_path_str = request.path_to_file
|
|
226
|
+
else:
|
|
227
|
+
# This should never happen due to validation above, but type checker needs it
|
|
228
|
+
msg = "No valid file path provided"
|
|
229
|
+
logger.error(msg)
|
|
230
|
+
return OpenAssociatedFileResultFailure()
|
|
231
|
+
|
|
232
|
+
# At this point, file_path_str is guaranteed to be a string
|
|
233
|
+
if file_path_str is None:
|
|
234
|
+
msg = "No valid file path provided"
|
|
235
|
+
logger.error(msg)
|
|
236
|
+
return OpenAssociatedFileResultFailure()
|
|
237
|
+
|
|
76
238
|
# Sanitize and validate the file path
|
|
77
239
|
try:
|
|
78
|
-
path
|
|
240
|
+
# Resolve the path (no workspace fallback for open requests)
|
|
241
|
+
path = self._resolve_file_path(file_path_str, workspace_only=False)
|
|
79
242
|
except (ValueError, RuntimeError):
|
|
80
|
-
details = f"Invalid file path: '{
|
|
243
|
+
details = f"Invalid file path: '{file_path_str}'"
|
|
81
244
|
logger.info(details)
|
|
82
245
|
return OpenAssociatedFileResultFailure()
|
|
83
246
|
|
|
@@ -90,12 +253,12 @@ class OSManager:
|
|
|
90
253
|
|
|
91
254
|
try:
|
|
92
255
|
platform_name = sys.platform
|
|
93
|
-
if self.is_windows:
|
|
256
|
+
if self.is_windows():
|
|
94
257
|
# Linter complains but this is the recommended way on Windows
|
|
95
258
|
# We can ignore this warning as we've validated the path
|
|
96
259
|
os.startfile(str(path)) # noqa: S606 # pyright: ignore[reportAttributeAccessIssue]
|
|
97
260
|
logger.info("Started file on Windows: %s", path)
|
|
98
|
-
elif self.is_mac:
|
|
261
|
+
elif self.is_mac():
|
|
99
262
|
# On macOS, open should be in a standard location
|
|
100
263
|
subprocess.run( # noqa: S603
|
|
101
264
|
["/usr/bin/open", str(path)],
|
|
@@ -104,7 +267,7 @@ class OSManager:
|
|
|
104
267
|
text=True,
|
|
105
268
|
)
|
|
106
269
|
logger.info("Started file on macOS: %s", path)
|
|
107
|
-
elif self.is_linux:
|
|
270
|
+
elif self.is_linux():
|
|
108
271
|
# Use full path to xdg-open to satisfy linter
|
|
109
272
|
# Common locations for xdg-open:
|
|
110
273
|
xdg_paths = ["/usr/bin/xdg-open", "/bin/xdg-open", "/usr/local/bin/xdg-open"]
|
|
@@ -139,6 +302,214 @@ class OSManager:
|
|
|
139
302
|
logger.error("Exception occurred when trying to open file: %s", type(e).__name__)
|
|
140
303
|
return OpenAssociatedFileResultFailure()
|
|
141
304
|
|
|
305
|
+
def on_list_directory_request(self, request: ListDirectoryRequest) -> ResultPayload: # noqa: C901, PLR0911
|
|
306
|
+
"""Handle a request to list directory contents."""
|
|
307
|
+
try:
|
|
308
|
+
# Get the directory path to list
|
|
309
|
+
if request.directory_path is None:
|
|
310
|
+
directory = self._get_workspace_path()
|
|
311
|
+
# Handle paths consistently - always resolve relative paths relative to current directory
|
|
312
|
+
elif Path(request.directory_path).is_absolute() or request.directory_path.startswith("~"):
|
|
313
|
+
# Expand tilde and environment variables for absolute paths or paths starting with ~
|
|
314
|
+
directory = self._expand_path(request.directory_path)
|
|
315
|
+
else:
|
|
316
|
+
# Both workspace and system-wide modes resolve relative to current directory
|
|
317
|
+
directory = (self._get_workspace_path() / request.directory_path).resolve()
|
|
318
|
+
|
|
319
|
+
# Check if directory exists
|
|
320
|
+
if not directory.exists():
|
|
321
|
+
msg = f"Directory does not exist: {directory}"
|
|
322
|
+
logger.error(msg)
|
|
323
|
+
return ListDirectoryResultFailure()
|
|
324
|
+
if not directory.is_dir():
|
|
325
|
+
msg = f"Directory is not a directory: {directory}"
|
|
326
|
+
logger.error(msg)
|
|
327
|
+
return ListDirectoryResultFailure()
|
|
328
|
+
|
|
329
|
+
# Check workspace constraints
|
|
330
|
+
is_workspace_path, relative_or_abs_path = self._validate_workspace_path(directory)
|
|
331
|
+
if request.workspace_only and not is_workspace_path:
|
|
332
|
+
msg = f"Directory is outside workspace: {directory}"
|
|
333
|
+
logger.error(msg)
|
|
334
|
+
return ListDirectoryResultFailure()
|
|
335
|
+
|
|
336
|
+
entries = []
|
|
337
|
+
try:
|
|
338
|
+
# List directory contents
|
|
339
|
+
for entry in directory.iterdir():
|
|
340
|
+
# Skip hidden files if not requested
|
|
341
|
+
if not request.show_hidden and entry.name.startswith("."):
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
stat = entry.stat()
|
|
346
|
+
# Get path relative to workspace if within workspace
|
|
347
|
+
is_entry_in_workspace, entry_path = self._validate_workspace_path(entry)
|
|
348
|
+
entries.append(
|
|
349
|
+
FileSystemEntry(
|
|
350
|
+
name=entry.name,
|
|
351
|
+
path=str(entry_path),
|
|
352
|
+
is_dir=entry.is_dir(),
|
|
353
|
+
size=stat.st_size,
|
|
354
|
+
modified_time=stat.st_mtime,
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
except (OSError, PermissionError) as e:
|
|
358
|
+
msg = f"Could not stat entry {entry}: {e}"
|
|
359
|
+
logger.warning(msg)
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
except (OSError, PermissionError) as e:
|
|
363
|
+
msg = f"Error listing directory {directory}: {e}"
|
|
364
|
+
logger.error(msg)
|
|
365
|
+
return ListDirectoryResultFailure()
|
|
366
|
+
|
|
367
|
+
# Return appropriate path format based on mode
|
|
368
|
+
if request.workspace_only:
|
|
369
|
+
# In workspace mode, return relative path if within workspace, absolute if outside
|
|
370
|
+
return ListDirectoryResultSuccess(
|
|
371
|
+
entries=entries, current_path=str(relative_or_abs_path), is_workspace_path=is_workspace_path
|
|
372
|
+
)
|
|
373
|
+
# In system-wide mode, always return the full absolute path
|
|
374
|
+
return ListDirectoryResultSuccess(
|
|
375
|
+
entries=entries, current_path=str(directory), is_workspace_path=is_workspace_path
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
except Exception as e:
|
|
379
|
+
msg = f"Unexpected error in list_directory: {type(e).__name__}: {e}"
|
|
380
|
+
logger.error(msg)
|
|
381
|
+
return ListDirectoryResultFailure()
|
|
382
|
+
|
|
383
|
+
def on_read_file_request(self, request: ReadFileRequest) -> ResultPayload:
|
|
384
|
+
"""Handle a request to read file contents with automatic text/binary detection."""
|
|
385
|
+
# Initialize variables that might be used in exception handlers
|
|
386
|
+
file_path: Path | None = None
|
|
387
|
+
file_path_str: str | None = None
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
# Validate request and get file path
|
|
391
|
+
file_path, file_path_str = self._validate_read_file_request(request)
|
|
392
|
+
|
|
393
|
+
# Read file content
|
|
394
|
+
content, encoding, mime_type, compression_encoding, file_size = self._read_file_content(file_path, request)
|
|
395
|
+
|
|
396
|
+
return ReadFileResultSuccess(
|
|
397
|
+
content=content,
|
|
398
|
+
file_size=file_size,
|
|
399
|
+
mime_type=mime_type,
|
|
400
|
+
encoding=encoding,
|
|
401
|
+
compression_encoding=compression_encoding,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
except (ValueError, FileNotFoundError) as e:
|
|
405
|
+
file_info = f" for file: {file_path}" if file_path is not None else ""
|
|
406
|
+
msg = f"Validation error in read_file{file_info}: {e}"
|
|
407
|
+
logger.error(msg)
|
|
408
|
+
return ReadFileResultFailure()
|
|
409
|
+
except Exception as e:
|
|
410
|
+
# Try to include file path in error message if available
|
|
411
|
+
path_info = ""
|
|
412
|
+
if file_path is not None:
|
|
413
|
+
path_info = f" for {file_path}"
|
|
414
|
+
elif file_path_str is not None:
|
|
415
|
+
path_info = f" for {file_path_str}"
|
|
416
|
+
|
|
417
|
+
msg = f"Unexpected error in read_file{path_info}: {type(e).__name__}: {e}"
|
|
418
|
+
logger.error(msg)
|
|
419
|
+
return ReadFileResultFailure()
|
|
420
|
+
|
|
421
|
+
def _read_file_content(
|
|
422
|
+
self, file_path: Path, request: ReadFileRequest
|
|
423
|
+
) -> tuple[bytes | str, str | None, str, str | None, int]:
|
|
424
|
+
"""Read file content and return content, encoding, mime_type, compression_encoding, and file_size."""
|
|
425
|
+
# Get file size
|
|
426
|
+
file_size = file_path.stat().st_size
|
|
427
|
+
|
|
428
|
+
# Determine MIME type and compression encoding
|
|
429
|
+
mime_type, compression_encoding = mimetypes.guess_type(str(file_path), strict=True)
|
|
430
|
+
if mime_type is None:
|
|
431
|
+
mime_type = "text/plain"
|
|
432
|
+
|
|
433
|
+
# Determine if file is binary
|
|
434
|
+
try:
|
|
435
|
+
is_binary_file = is_binary(str(file_path))
|
|
436
|
+
except Exception as e:
|
|
437
|
+
msg = f"binaryornot detection failed for {file_path}: {e}"
|
|
438
|
+
logger.warning(msg)
|
|
439
|
+
is_binary_file = not mime_type.startswith(
|
|
440
|
+
("text/", "application/json", "application/xml", "application/yaml")
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Read file content
|
|
444
|
+
if not is_binary_file:
|
|
445
|
+
content, encoding = self._read_text_file(file_path, request.encoding)
|
|
446
|
+
else:
|
|
447
|
+
content, encoding = self._read_binary_file(file_path, mime_type)
|
|
448
|
+
|
|
449
|
+
return content, encoding, mime_type, compression_encoding, file_size
|
|
450
|
+
|
|
451
|
+
def _read_text_file(self, file_path: Path, requested_encoding: str) -> tuple[bytes | str, str | None]:
|
|
452
|
+
"""Read file as text with fallback encodings."""
|
|
453
|
+
try:
|
|
454
|
+
with file_path.open(encoding=requested_encoding) as f:
|
|
455
|
+
return f.read(), requested_encoding
|
|
456
|
+
except UnicodeDecodeError:
|
|
457
|
+
try:
|
|
458
|
+
with file_path.open(encoding="utf-8") as f:
|
|
459
|
+
return f.read(), "utf-8"
|
|
460
|
+
except UnicodeDecodeError:
|
|
461
|
+
with file_path.open("rb") as f:
|
|
462
|
+
return f.read(), None
|
|
463
|
+
|
|
464
|
+
def _read_binary_file(self, file_path: Path, mime_type: str) -> tuple[bytes | str, None]:
|
|
465
|
+
"""Read file as binary, with special handling for images."""
|
|
466
|
+
with file_path.open("rb") as f:
|
|
467
|
+
content = f.read()
|
|
468
|
+
|
|
469
|
+
if mime_type.startswith("image/"):
|
|
470
|
+
content = self._handle_image_content(content, file_path, mime_type)
|
|
471
|
+
|
|
472
|
+
return content, None
|
|
473
|
+
|
|
474
|
+
def _handle_image_content(self, content: bytes, file_path: Path, mime_type: str) -> str:
|
|
475
|
+
"""Handle image content by creating previews or returning static URLs."""
|
|
476
|
+
# Store original bytes for preview creation
|
|
477
|
+
original_image_bytes = content
|
|
478
|
+
|
|
479
|
+
# Check if file is already in the static files directory
|
|
480
|
+
config_manager = GriptapeNodes.ConfigManager()
|
|
481
|
+
static_files_directory = config_manager.get_config_value("static_files_directory", default="staticfiles")
|
|
482
|
+
static_dir = config_manager.workspace_path / static_files_directory
|
|
483
|
+
|
|
484
|
+
try:
|
|
485
|
+
# Check if file is within the static files directory
|
|
486
|
+
file_relative_to_static = file_path.relative_to(static_dir)
|
|
487
|
+
# File is in static directory, construct URL directly
|
|
488
|
+
static_url = f"http://localhost:8124/static/{file_relative_to_static}"
|
|
489
|
+
msg = f"Image already in static directory, returning URL: {static_url}"
|
|
490
|
+
logger.debug(msg)
|
|
491
|
+
except ValueError:
|
|
492
|
+
# File is not in static directory, create small preview
|
|
493
|
+
from griptape_nodes.utils.image_preview import create_image_preview_from_bytes
|
|
494
|
+
|
|
495
|
+
preview_data_url = create_image_preview_from_bytes(
|
|
496
|
+
original_image_bytes, # type: ignore[arg-type]
|
|
497
|
+
max_width=200,
|
|
498
|
+
max_height=200,
|
|
499
|
+
quality=85,
|
|
500
|
+
image_format="WEBP",
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
if preview_data_url:
|
|
504
|
+
logger.debug("Image preview created (file not moved)")
|
|
505
|
+
return preview_data_url
|
|
506
|
+
# Fallback to data URL if preview creation fails
|
|
507
|
+
data_url = f"data:{mime_type};base64,{base64.b64encode(original_image_bytes).decode('utf-8')}"
|
|
508
|
+
logger.debug("Fallback to full image data URL")
|
|
509
|
+
return data_url
|
|
510
|
+
else:
|
|
511
|
+
return static_url
|
|
512
|
+
|
|
142
513
|
@staticmethod
|
|
143
514
|
def get_disk_space_info(path: Path) -> DiskSpaceInfo:
|
|
144
515
|
"""Get disk space information for a given path.
|
|
@@ -197,4 +568,138 @@ class OSManager:
|
|
|
197
568
|
|
|
198
569
|
return error_msg # noqa: TRY300
|
|
199
570
|
except OSError:
|
|
200
|
-
return f"
|
|
571
|
+
return f"Could not determine disk space at {path}. Please check disk space manually."
|
|
572
|
+
|
|
573
|
+
@staticmethod
|
|
574
|
+
def cleanup_directory_if_needed(full_directory_path: Path, max_size_gb: float) -> bool:
|
|
575
|
+
"""Check directory size and cleanup old files if needed.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
full_directory_path: Path to the directory to check and clean
|
|
579
|
+
max_size_gb: Target size in GB
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
True if cleanup was performed, False otherwise
|
|
583
|
+
"""
|
|
584
|
+
if max_size_gb < 0:
|
|
585
|
+
logger.warning(
|
|
586
|
+
"Asked to clean up directory to be below a negative threshold. Overriding to a size of 0 GB."
|
|
587
|
+
)
|
|
588
|
+
max_size_gb = 0
|
|
589
|
+
|
|
590
|
+
# Calculate current directory size
|
|
591
|
+
current_size_gb = OSManager._get_directory_size_gb(full_directory_path)
|
|
592
|
+
|
|
593
|
+
if current_size_gb <= max_size_gb:
|
|
594
|
+
return False
|
|
595
|
+
|
|
596
|
+
logger.info(
|
|
597
|
+
"Directory %s size (%.1f GB) exceeds limit (%s GB). Starting cleanup...",
|
|
598
|
+
full_directory_path,
|
|
599
|
+
current_size_gb,
|
|
600
|
+
max_size_gb,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# Perform cleanup
|
|
604
|
+
return OSManager._cleanup_old_files(full_directory_path, max_size_gb)
|
|
605
|
+
|
|
606
|
+
@staticmethod
|
|
607
|
+
def _get_directory_size_gb(path: Path) -> float:
|
|
608
|
+
"""Get total size of directory in GB.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
path: Path to the directory
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
Total size in GB
|
|
615
|
+
"""
|
|
616
|
+
total_size = 0.0
|
|
617
|
+
|
|
618
|
+
if not path.exists():
|
|
619
|
+
logger.error("Directory %s does not exist. Skipping cleanup.", path)
|
|
620
|
+
return 0.0
|
|
621
|
+
|
|
622
|
+
for _, _, files in os.walk(path):
|
|
623
|
+
for f in files:
|
|
624
|
+
fp = path / f
|
|
625
|
+
if not fp.is_symlink():
|
|
626
|
+
total_size += fp.stat().st_size
|
|
627
|
+
return total_size / (1024 * 1024 * 1024) # Convert to GB
|
|
628
|
+
|
|
629
|
+
@staticmethod
|
|
630
|
+
def _cleanup_old_files(directory_path: Path, target_size_gb: float) -> bool:
|
|
631
|
+
"""Remove oldest files until directory is under target size.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
directory_path: Path to the directory to clean
|
|
635
|
+
target_size_gb: Target size in GB
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
True if files were removed, False otherwise
|
|
639
|
+
"""
|
|
640
|
+
if not directory_path.exists():
|
|
641
|
+
logger.error("Directory %s does not exist. Skipping cleanup.", directory_path)
|
|
642
|
+
return False
|
|
643
|
+
|
|
644
|
+
# Get all files with their modification times
|
|
645
|
+
files_with_times: list[tuple[Path, float]] = []
|
|
646
|
+
|
|
647
|
+
for file_path in directory_path.rglob("*"):
|
|
648
|
+
if file_path.is_file():
|
|
649
|
+
try:
|
|
650
|
+
mtime = file_path.stat().st_mtime
|
|
651
|
+
files_with_times.append((file_path, mtime))
|
|
652
|
+
except (OSError, FileNotFoundError) as err:
|
|
653
|
+
# Skip files that can't be accessed
|
|
654
|
+
logger.error(
|
|
655
|
+
"While cleaning up old files, saw file %s. File could not be accessed; skipping. Error: %s",
|
|
656
|
+
file_path,
|
|
657
|
+
err,
|
|
658
|
+
)
|
|
659
|
+
continue
|
|
660
|
+
|
|
661
|
+
if not files_with_times:
|
|
662
|
+
logger.error(
|
|
663
|
+
"Attempted to clean up files to get below a target directory size, but no suitable files were found that could be deleted."
|
|
664
|
+
)
|
|
665
|
+
return False
|
|
666
|
+
|
|
667
|
+
# Sort by modification time (oldest first)
|
|
668
|
+
files_with_times.sort(key=lambda x: x[1])
|
|
669
|
+
|
|
670
|
+
# Remove files until we're under the target size
|
|
671
|
+
removed_count = 0
|
|
672
|
+
|
|
673
|
+
for file_path, _ in files_with_times:
|
|
674
|
+
try:
|
|
675
|
+
# Delete the file.
|
|
676
|
+
file_path.unlink()
|
|
677
|
+
removed_count += 1
|
|
678
|
+
|
|
679
|
+
# Check if we're now under the target size
|
|
680
|
+
current_size_gb = OSManager._get_directory_size_gb(directory_path)
|
|
681
|
+
if current_size_gb <= target_size_gb:
|
|
682
|
+
# We're done!
|
|
683
|
+
break
|
|
684
|
+
|
|
685
|
+
except (OSError, FileNotFoundError) as err:
|
|
686
|
+
# Skip files that can't be deleted
|
|
687
|
+
logger.error(
|
|
688
|
+
"While cleaning up old files, attempted to delete file %s. File could not be deleted; skipping. Deletion error: %s",
|
|
689
|
+
file_path,
|
|
690
|
+
err,
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
if removed_count > 0:
|
|
694
|
+
final_size_gb = OSManager._get_directory_size_gb(directory_path)
|
|
695
|
+
logger.info(
|
|
696
|
+
"Cleaned up %d old files from %s. Directory size reduced to %.1f GB",
|
|
697
|
+
removed_count,
|
|
698
|
+
directory_path,
|
|
699
|
+
final_size_gb,
|
|
700
|
+
)
|
|
701
|
+
else:
|
|
702
|
+
# None deleted.
|
|
703
|
+
logger.error("Attempted to clean up old files from %s, but no files could be deleted.")
|
|
704
|
+
|
|
705
|
+
return removed_count > 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -15,7 +15,7 @@ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes, Version
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from griptape_nodes.node_library.library_registry import LibrarySchema
|
|
17
17
|
from griptape_nodes.retained_mode.managers.event_manager import EventManager
|
|
18
|
-
from griptape_nodes.retained_mode.managers.
|
|
18
|
+
from griptape_nodes.retained_mode.managers.library_lifecycle.library_status import LibraryStatus
|
|
19
19
|
|
|
20
20
|
logger = logging.getLogger("griptape_nodes")
|
|
21
21
|
|
|
@@ -24,7 +24,7 @@ class LibraryVersionCompatibilityIssue(NamedTuple):
|
|
|
24
24
|
"""Represents a library version compatibility issue found in a library."""
|
|
25
25
|
|
|
26
26
|
message: str
|
|
27
|
-
severity:
|
|
27
|
+
severity: LibraryStatus
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class LibraryVersionCompatibilityCheck(ABC):
|