griptape-nodes 0.64.10__py3-none-any.whl → 0.65.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/app.py +25 -5
- griptape_nodes/cli/commands/init.py +65 -54
- griptape_nodes/cli/commands/libraries.py +92 -85
- griptape_nodes/cli/commands/self.py +121 -0
- griptape_nodes/common/node_executor.py +2142 -101
- griptape_nodes/exe_types/base_iterative_nodes.py +1004 -0
- griptape_nodes/exe_types/connections.py +114 -19
- griptape_nodes/exe_types/core_types.py +225 -7
- griptape_nodes/exe_types/flow.py +3 -3
- griptape_nodes/exe_types/node_types.py +681 -225
- griptape_nodes/exe_types/param_components/README.md +414 -0
- griptape_nodes/exe_types/param_components/api_key_provider_parameter.py +200 -0
- griptape_nodes/exe_types/param_components/huggingface/huggingface_model_parameter.py +2 -0
- griptape_nodes/exe_types/param_components/huggingface/huggingface_repo_file_parameter.py +79 -5
- griptape_nodes/exe_types/param_types/parameter_button.py +443 -0
- griptape_nodes/machines/control_flow.py +77 -38
- griptape_nodes/machines/dag_builder.py +148 -70
- griptape_nodes/machines/parallel_resolution.py +61 -35
- griptape_nodes/machines/sequential_resolution.py +11 -113
- griptape_nodes/retained_mode/events/app_events.py +1 -0
- griptape_nodes/retained_mode/events/base_events.py +16 -13
- griptape_nodes/retained_mode/events/connection_events.py +3 -0
- griptape_nodes/retained_mode/events/execution_events.py +35 -0
- griptape_nodes/retained_mode/events/flow_events.py +15 -2
- griptape_nodes/retained_mode/events/library_events.py +347 -0
- griptape_nodes/retained_mode/events/node_events.py +48 -0
- griptape_nodes/retained_mode/events/os_events.py +86 -3
- griptape_nodes/retained_mode/events/project_events.py +15 -1
- griptape_nodes/retained_mode/events/workflow_events.py +48 -1
- griptape_nodes/retained_mode/griptape_nodes.py +6 -2
- griptape_nodes/retained_mode/managers/config_manager.py +10 -8
- griptape_nodes/retained_mode/managers/event_manager.py +168 -0
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +2 -0
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/old_xdg_location_warning_problem.py +43 -0
- griptape_nodes/retained_mode/managers/flow_manager.py +664 -123
- griptape_nodes/retained_mode/managers/library_manager.py +1143 -139
- griptape_nodes/retained_mode/managers/model_manager.py +2 -3
- griptape_nodes/retained_mode/managers/node_manager.py +148 -25
- griptape_nodes/retained_mode/managers/object_manager.py +3 -1
- griptape_nodes/retained_mode/managers/operation_manager.py +3 -1
- griptape_nodes/retained_mode/managers/os_manager.py +1158 -122
- griptape_nodes/retained_mode/managers/secrets_manager.py +2 -3
- griptape_nodes/retained_mode/managers/settings.py +21 -1
- griptape_nodes/retained_mode/managers/sync_manager.py +2 -3
- griptape_nodes/retained_mode/managers/workflow_manager.py +358 -104
- griptape_nodes/retained_mode/retained_mode.py +3 -3
- griptape_nodes/traits/button.py +44 -2
- griptape_nodes/traits/file_system_picker.py +2 -2
- griptape_nodes/utils/file_utils.py +101 -0
- griptape_nodes/utils/git_utils.py +1226 -0
- griptape_nodes/utils/library_utils.py +122 -0
- {griptape_nodes-0.64.10.dist-info → griptape_nodes-0.65.0.dist-info}/METADATA +2 -1
- {griptape_nodes-0.64.10.dist-info → griptape_nodes-0.65.0.dist-info}/RECORD +55 -47
- {griptape_nodes-0.64.10.dist-info → griptape_nodes-0.65.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.64.10.dist-info → griptape_nodes-0.65.0.dist-info}/entry_points.txt +0 -0
|
@@ -13,7 +13,7 @@ from collections import defaultdict
|
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from importlib.resources import files
|
|
15
15
|
from pathlib import Path
|
|
16
|
-
from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Generic, NamedTuple, TypeVar, cast
|
|
17
17
|
|
|
18
18
|
from packaging.requirements import InvalidRequirement, Requirement
|
|
19
19
|
from pydantic import ValidationError
|
|
@@ -23,6 +23,7 @@ from rich.console import Console
|
|
|
23
23
|
from rich.panel import Panel
|
|
24
24
|
from rich.table import Table
|
|
25
25
|
from rich.text import Text
|
|
26
|
+
from semver import Version
|
|
26
27
|
from xdg_base_dirs import xdg_data_home
|
|
27
28
|
|
|
28
29
|
from griptape_nodes.exe_types.node_types import BaseNode
|
|
@@ -45,7 +46,7 @@ from griptape_nodes.retained_mode.events.app_events import (
|
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
# Runtime imports for ResultDetails since it's used at runtime
|
|
48
|
-
from griptape_nodes.retained_mode.events.base_events import AppEvent, ResultDetails
|
|
49
|
+
from griptape_nodes.retained_mode.events.base_events import AppEvent, ResultDetails, ResultPayloadFailure
|
|
49
50
|
from griptape_nodes.retained_mode.events.config_events import (
|
|
50
51
|
GetConfigCategoryRequest,
|
|
51
52
|
GetConfigCategoryResultSuccess,
|
|
@@ -53,6 +54,12 @@ from griptape_nodes.retained_mode.events.config_events import (
|
|
|
53
54
|
SetConfigCategoryResultSuccess,
|
|
54
55
|
)
|
|
55
56
|
from griptape_nodes.retained_mode.events.library_events import (
|
|
57
|
+
CheckLibraryUpdateRequest,
|
|
58
|
+
CheckLibraryUpdateResultFailure,
|
|
59
|
+
CheckLibraryUpdateResultSuccess,
|
|
60
|
+
DownloadLibraryRequest,
|
|
61
|
+
DownloadLibraryResultFailure,
|
|
62
|
+
DownloadLibraryResultSuccess,
|
|
56
63
|
GetAllInfoForAllLibrariesRequest,
|
|
57
64
|
GetAllInfoForAllLibrariesResultFailure,
|
|
58
65
|
GetAllInfoForAllLibrariesResultSuccess,
|
|
@@ -65,6 +72,12 @@ from griptape_nodes.retained_mode.events.library_events import (
|
|
|
65
72
|
GetNodeMetadataFromLibraryRequest,
|
|
66
73
|
GetNodeMetadataFromLibraryResultFailure,
|
|
67
74
|
GetNodeMetadataFromLibraryResultSuccess,
|
|
75
|
+
InspectLibraryRepoRequest,
|
|
76
|
+
InspectLibraryRepoResultFailure,
|
|
77
|
+
InspectLibraryRepoResultSuccess,
|
|
78
|
+
InstallLibraryDependenciesRequest,
|
|
79
|
+
InstallLibraryDependenciesResultFailure,
|
|
80
|
+
InstallLibraryDependenciesResultSuccess,
|
|
68
81
|
ListCapableLibraryEventHandlersRequest,
|
|
69
82
|
ListCapableLibraryEventHandlersResultFailure,
|
|
70
83
|
ListCapableLibraryEventHandlersResultSuccess,
|
|
@@ -93,11 +106,24 @@ from griptape_nodes.retained_mode.events.library_events import (
|
|
|
93
106
|
ReloadAllLibrariesRequest,
|
|
94
107
|
ReloadAllLibrariesResultFailure,
|
|
95
108
|
ReloadAllLibrariesResultSuccess,
|
|
109
|
+
SwitchLibraryRefRequest,
|
|
110
|
+
SwitchLibraryRefResultFailure,
|
|
111
|
+
SwitchLibraryRefResultSuccess,
|
|
112
|
+
SyncLibrariesRequest,
|
|
113
|
+
SyncLibrariesResultFailure,
|
|
114
|
+
SyncLibrariesResultSuccess,
|
|
96
115
|
UnloadLibraryFromRegistryRequest,
|
|
97
116
|
UnloadLibraryFromRegistryResultFailure,
|
|
98
117
|
UnloadLibraryFromRegistryResultSuccess,
|
|
118
|
+
UpdateLibraryRequest,
|
|
119
|
+
UpdateLibraryResultFailure,
|
|
120
|
+
UpdateLibraryResultSuccess,
|
|
99
121
|
)
|
|
100
122
|
from griptape_nodes.retained_mode.events.object_events import ClearAllObjectStateRequest
|
|
123
|
+
from griptape_nodes.retained_mode.events.os_events import (
|
|
124
|
+
DeleteFileRequest,
|
|
125
|
+
DeleteFileResultFailure,
|
|
126
|
+
)
|
|
101
127
|
from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
|
|
102
128
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
103
129
|
from griptape_nodes.retained_mode.managers.fitness_problems.libraries import (
|
|
@@ -105,10 +131,8 @@ from griptape_nodes.retained_mode.managers.fitness_problems.libraries import (
|
|
|
105
131
|
AfterLibraryCallbackProblem,
|
|
106
132
|
BeforeLibraryCallbackProblem,
|
|
107
133
|
CreateConfigCategoryProblem,
|
|
108
|
-
DependencyInstallationFailedProblem,
|
|
109
134
|
DuplicateLibraryProblem,
|
|
110
135
|
EngineVersionErrorProblem,
|
|
111
|
-
InsufficientDiskSpaceProblem,
|
|
112
136
|
InvalidVersionStringProblem,
|
|
113
137
|
LibraryJsonDecodeProblem,
|
|
114
138
|
LibraryLoadExceptionProblem,
|
|
@@ -119,9 +143,9 @@ from griptape_nodes.retained_mode.managers.fitness_problems.libraries import (
|
|
|
119
143
|
NodeClassNotBaseNodeProblem,
|
|
120
144
|
NodeClassNotFoundProblem,
|
|
121
145
|
NodeModuleImportProblem,
|
|
146
|
+
OldXdgLocationWarningProblem,
|
|
122
147
|
SandboxDirectoryMissingProblem,
|
|
123
148
|
UpdateConfigCategoryProblem,
|
|
124
|
-
VenvCreationFailedProblem,
|
|
125
149
|
)
|
|
126
150
|
from griptape_nodes.retained_mode.managers.library_lifecycle.library_directory import LibraryDirectory
|
|
127
151
|
from griptape_nodes.retained_mode.managers.library_lifecycle.library_provenance.local_file import (
|
|
@@ -129,8 +153,32 @@ from griptape_nodes.retained_mode.managers.library_lifecycle.library_provenance.
|
|
|
129
153
|
)
|
|
130
154
|
from griptape_nodes.retained_mode.managers.library_lifecycle.library_status import LibraryStatus
|
|
131
155
|
from griptape_nodes.retained_mode.managers.os_manager import OSManager
|
|
156
|
+
from griptape_nodes.retained_mode.managers.settings import LIBRARIES_TO_DOWNLOAD_KEY, LIBRARIES_TO_REGISTER_KEY
|
|
132
157
|
from griptape_nodes.utils.async_utils import subprocess_run
|
|
133
158
|
from griptape_nodes.utils.dict_utils import merge_dicts
|
|
159
|
+
from griptape_nodes.utils.file_utils import find_file_in_directory
|
|
160
|
+
from griptape_nodes.utils.git_utils import (
|
|
161
|
+
GitCloneError,
|
|
162
|
+
GitPullError,
|
|
163
|
+
GitRefError,
|
|
164
|
+
GitRemoteError,
|
|
165
|
+
GitRepositoryError,
|
|
166
|
+
clone_repository,
|
|
167
|
+
extract_repo_name_from_url,
|
|
168
|
+
get_current_ref,
|
|
169
|
+
get_git_remote,
|
|
170
|
+
get_local_commit_sha,
|
|
171
|
+
is_git_url,
|
|
172
|
+
parse_git_url_with_ref,
|
|
173
|
+
switch_branch_or_tag,
|
|
174
|
+
update_library_git,
|
|
175
|
+
)
|
|
176
|
+
from griptape_nodes.utils.library_utils import (
|
|
177
|
+
LIBRARY_GIT_URLS,
|
|
178
|
+
clone_and_get_library_version,
|
|
179
|
+
filter_old_xdg_library_paths,
|
|
180
|
+
is_monorepo,
|
|
181
|
+
)
|
|
134
182
|
from griptape_nodes.utils.uv_utils import find_uv_bin
|
|
135
183
|
from griptape_nodes.utils.version_utils import get_complete_version_string
|
|
136
184
|
|
|
@@ -145,9 +193,38 @@ if TYPE_CHECKING:
|
|
|
145
193
|
logger = logging.getLogger("griptape_nodes")
|
|
146
194
|
console = Console()
|
|
147
195
|
|
|
196
|
+
# Directories to exclude when scanning for Python source files (in addition to any directory starting with '.')
|
|
197
|
+
EXCLUDED_SCAN_DIRECTORIES = frozenset({"venv", "__pycache__"})
|
|
198
|
+
|
|
148
199
|
TRegisteredEventData = TypeVar("TRegisteredEventData")
|
|
149
200
|
|
|
150
201
|
|
|
202
|
+
class LibraryGitOperationContext(NamedTuple):
|
|
203
|
+
"""Context information for git operations on a library."""
|
|
204
|
+
|
|
205
|
+
library: Library
|
|
206
|
+
old_version: str
|
|
207
|
+
library_file_path: str
|
|
208
|
+
library_dir: Path
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class LibraryUpdateInfo(NamedTuple):
|
|
212
|
+
"""Information about a library pending update."""
|
|
213
|
+
|
|
214
|
+
library_name: str
|
|
215
|
+
old_version: str
|
|
216
|
+
new_version: str
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class LibraryUpdateResult(NamedTuple):
|
|
220
|
+
"""Result of updating a single library."""
|
|
221
|
+
|
|
222
|
+
library_name: str
|
|
223
|
+
old_version: str
|
|
224
|
+
new_version: str
|
|
225
|
+
result: ResultPayload
|
|
226
|
+
|
|
227
|
+
|
|
151
228
|
class LibraryManager:
|
|
152
229
|
SANDBOX_LIBRARY_NAME = "Sandbox Library"
|
|
153
230
|
LIBRARY_CONFIG_FILENAME = "griptape_nodes_library.json"
|
|
@@ -255,6 +332,15 @@ class LibraryManager:
|
|
|
255
332
|
)
|
|
256
333
|
event_manager.assign_manager_to_request_type(ReloadAllLibrariesRequest, self.reload_libraries_request)
|
|
257
334
|
event_manager.assign_manager_to_request_type(LoadLibrariesRequest, self.load_libraries_request)
|
|
335
|
+
event_manager.assign_manager_to_request_type(CheckLibraryUpdateRequest, self.check_library_update_request)
|
|
336
|
+
event_manager.assign_manager_to_request_type(UpdateLibraryRequest, self.update_library_request)
|
|
337
|
+
event_manager.assign_manager_to_request_type(SwitchLibraryRefRequest, self.switch_library_ref_request)
|
|
338
|
+
event_manager.assign_manager_to_request_type(DownloadLibraryRequest, self.download_library_request)
|
|
339
|
+
event_manager.assign_manager_to_request_type(
|
|
340
|
+
InstallLibraryDependenciesRequest, self.install_library_dependencies_request
|
|
341
|
+
)
|
|
342
|
+
event_manager.assign_manager_to_request_type(SyncLibrariesRequest, self.sync_libraries_request)
|
|
343
|
+
event_manager.assign_manager_to_request_type(InspectLibraryRepoRequest, self.inspect_library_repo_request)
|
|
258
344
|
|
|
259
345
|
event_manager.add_listener_to_app_event(
|
|
260
346
|
AppInitializationComplete,
|
|
@@ -417,7 +503,6 @@ class LibraryManager:
|
|
|
417
503
|
library = LibraryRegistry.get_library(name=request.library)
|
|
418
504
|
except KeyError:
|
|
419
505
|
details = f"Attempted to list node types in a Library named '{request.library}'. Failed because no Library with that name was registered."
|
|
420
|
-
logger.error(details)
|
|
421
506
|
|
|
422
507
|
result = ListNodeTypesInLibraryResultFailure(result_details=details)
|
|
423
508
|
return result
|
|
@@ -440,7 +525,6 @@ class LibraryManager:
|
|
|
440
525
|
library = LibraryRegistry.get_library(name=request.library)
|
|
441
526
|
except KeyError:
|
|
442
527
|
details = f"Attempted to get metadata for Library '{request.library}'. Failed because no Library with that name was registered."
|
|
443
|
-
logger.error(details)
|
|
444
528
|
|
|
445
529
|
result = GetLibraryMetadataResultFailure(result_details=details)
|
|
446
530
|
return result
|
|
@@ -466,7 +550,6 @@ class LibraryManager:
|
|
|
466
550
|
# Check if the file exists
|
|
467
551
|
if not json_path.exists():
|
|
468
552
|
details = f"Attempted to load Library JSON file. Failed because no file could be found at the specified path: {json_path}"
|
|
469
|
-
logger.error(details)
|
|
470
553
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
471
554
|
library_path=file_path,
|
|
472
555
|
library_name=None,
|
|
@@ -481,7 +564,6 @@ class LibraryManager:
|
|
|
481
564
|
library_json = json.load(f)
|
|
482
565
|
except json.JSONDecodeError:
|
|
483
566
|
details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' was improperly formatted."
|
|
484
|
-
logger.error(details)
|
|
485
567
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
486
568
|
library_path=file_path,
|
|
487
569
|
library_name=None,
|
|
@@ -491,7 +573,6 @@ class LibraryManager:
|
|
|
491
573
|
)
|
|
492
574
|
except Exception as err:
|
|
493
575
|
details = f"Attempted to load Library JSON file from location '{json_path}'. Failed because an exception occurred: {err}"
|
|
494
|
-
logger.error(details)
|
|
495
576
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
496
577
|
library_path=file_path,
|
|
497
578
|
library_name=None,
|
|
@@ -516,7 +597,6 @@ class LibraryManager:
|
|
|
516
597
|
problem = LibrarySchemaValidationProblem(location=loc, error_type=error_type, message=msg)
|
|
517
598
|
problems.append(problem)
|
|
518
599
|
details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' failed to match the library schema due to: {err}"
|
|
519
|
-
logger.error(details)
|
|
520
600
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
521
601
|
library_path=file_path,
|
|
522
602
|
library_name=library_name,
|
|
@@ -526,7 +606,6 @@ class LibraryManager:
|
|
|
526
606
|
)
|
|
527
607
|
except Exception as err:
|
|
528
608
|
details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' failed to match the library schema due to: {err}"
|
|
529
|
-
logger.error(details)
|
|
530
609
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
531
610
|
library_path=file_path,
|
|
532
611
|
library_name=library_name,
|
|
@@ -535,9 +614,18 @@ class LibraryManager:
|
|
|
535
614
|
result_details=details,
|
|
536
615
|
)
|
|
537
616
|
|
|
617
|
+
# Get git remote and ref if this library is in a git repository
|
|
618
|
+
library_dir = json_path.parent.absolute()
|
|
619
|
+
git_remote = get_git_remote(library_dir)
|
|
620
|
+
git_ref = get_current_ref(library_dir)
|
|
621
|
+
|
|
538
622
|
details = f"Successfully loaded library metadata from JSON file at {json_path}"
|
|
539
623
|
return LoadLibraryMetadataFromFileResultSuccess(
|
|
540
|
-
library_schema=library_data,
|
|
624
|
+
library_schema=library_data,
|
|
625
|
+
file_path=file_path,
|
|
626
|
+
git_remote=git_remote,
|
|
627
|
+
git_ref=git_ref,
|
|
628
|
+
result_details=details,
|
|
541
629
|
)
|
|
542
630
|
|
|
543
631
|
def load_metadata_for_all_libraries_request(self, request: LoadMetadataForAllLibrariesRequest) -> ResultPayload: # noqa: ARG002
|
|
@@ -683,9 +771,17 @@ class LibraryManager:
|
|
|
683
771
|
nodes=node_definitions,
|
|
684
772
|
)
|
|
685
773
|
|
|
774
|
+
# Get git remote and ref if the sandbox directory is in a git repository
|
|
775
|
+
git_remote = get_git_remote(sandbox_library_dir)
|
|
776
|
+
git_ref = get_current_ref(sandbox_library_dir)
|
|
777
|
+
|
|
686
778
|
details = f"Successfully generated sandbox library metadata with {len(node_definitions)} nodes from {sandbox_library_dir}"
|
|
687
779
|
return LoadLibraryMetadataFromFileResultSuccess(
|
|
688
|
-
library_schema=library_schema,
|
|
780
|
+
library_schema=library_schema,
|
|
781
|
+
file_path=str(sandbox_library_dir),
|
|
782
|
+
git_remote=git_remote,
|
|
783
|
+
git_ref=git_ref,
|
|
784
|
+
result_details=details,
|
|
689
785
|
)
|
|
690
786
|
|
|
691
787
|
def get_node_metadata_from_library_request(self, request: GetNodeMetadataFromLibraryRequest) -> ResultPayload:
|
|
@@ -719,7 +815,6 @@ class LibraryManager:
|
|
|
719
815
|
library = LibraryRegistry.get_library(name=request.library)
|
|
720
816
|
except KeyError:
|
|
721
817
|
details = f"Attempted to get categories in a Library named '{request.library}'. Failed because no Library with that name was registered."
|
|
722
|
-
logger.error(details)
|
|
723
818
|
result = ListCategoriesInLibraryResultFailure(result_details=details)
|
|
724
819
|
return result
|
|
725
820
|
|
|
@@ -744,7 +839,6 @@ class LibraryManager:
|
|
|
744
839
|
problems=[LibraryNotFoundProblem(library_path=file_path)],
|
|
745
840
|
)
|
|
746
841
|
details = f"Attempted to load Library JSON file. Failed because no file could be found at the specified path: {json_path}"
|
|
747
|
-
logger.error(details)
|
|
748
842
|
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
749
843
|
|
|
750
844
|
# Use the new metadata loading functionality
|
|
@@ -776,7 +870,6 @@ class LibraryManager:
|
|
|
776
870
|
problems=[InvalidVersionStringProblem(version_string=str(library_data.metadata.library_version))],
|
|
777
871
|
)
|
|
778
872
|
details = f"Attempted to load Library '{library_data.name}' JSON file from '{json_path}'. Failed because version string '{library_data.metadata.library_version}' wasn't valid. Must be in major.minor.patch format."
|
|
779
|
-
logger.error(details)
|
|
780
873
|
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
781
874
|
|
|
782
875
|
# Get the directory containing the JSON file to resolve relative paths
|
|
@@ -784,6 +877,39 @@ class LibraryManager:
|
|
|
784
877
|
# Add the directory to the Python path to allow for relative imports
|
|
785
878
|
sys.path.insert(0, str(base_dir))
|
|
786
879
|
|
|
880
|
+
# Install dependencies and add library venv to sys.path if library has dependencies
|
|
881
|
+
if library_data.metadata.dependencies and library_data.metadata.dependencies.pip_dependencies:
|
|
882
|
+
venv_path = self._get_library_venv_path(library_data.name, file_path)
|
|
883
|
+
|
|
884
|
+
install_request = InstallLibraryDependenciesRequest(library_file_path=file_path)
|
|
885
|
+
install_result = await self.install_library_dependencies_request(install_request)
|
|
886
|
+
|
|
887
|
+
if isinstance(install_result, InstallLibraryDependenciesResultFailure):
|
|
888
|
+
details = (
|
|
889
|
+
f"Failed to install dependencies for library '{library_data.name}': {install_result.result_details}"
|
|
890
|
+
)
|
|
891
|
+
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
892
|
+
|
|
893
|
+
if isinstance(install_result, InstallLibraryDependenciesResultSuccess):
|
|
894
|
+
logger.info(
|
|
895
|
+
"Installed %d dependencies for library '%s'",
|
|
896
|
+
install_result.dependencies_installed,
|
|
897
|
+
library_data.name,
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
# Add venv site-packages to sys.path so node imports can find dependencies
|
|
901
|
+
if venv_path.exists():
|
|
902
|
+
site_packages = str(
|
|
903
|
+
Path(
|
|
904
|
+
sysconfig.get_path(
|
|
905
|
+
"purelib",
|
|
906
|
+
vars={"base": str(venv_path), "platbase": str(venv_path)},
|
|
907
|
+
)
|
|
908
|
+
)
|
|
909
|
+
)
|
|
910
|
+
sys.path.insert(0, site_packages)
|
|
911
|
+
logger.debug("Added library '%s' venv to sys.path: %s", library_data.name, site_packages)
|
|
912
|
+
|
|
787
913
|
# Load the advanced library module if specified
|
|
788
914
|
advanced_library_instance = None
|
|
789
915
|
if library_data.advanced_library_path:
|
|
@@ -805,7 +931,6 @@ class LibraryManager:
|
|
|
805
931
|
],
|
|
806
932
|
)
|
|
807
933
|
details = f"Attempted to load Library '{library_data.name}' from '{json_path}'. Failed to load Advanced Library module: {err}"
|
|
808
|
-
logger.error(details)
|
|
809
934
|
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
810
935
|
|
|
811
936
|
# Create or get the library
|
|
@@ -828,91 +953,6 @@ class LibraryManager:
|
|
|
828
953
|
)
|
|
829
954
|
|
|
830
955
|
details = f"Attempted to load Library JSON file from '{json_path}'. Failed because a Library '{library_data.name}' already exists. Error: {err}."
|
|
831
|
-
logger.error(details)
|
|
832
|
-
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
833
|
-
|
|
834
|
-
# Install node library dependencies
|
|
835
|
-
try:
|
|
836
|
-
if library_data.metadata.dependencies and library_data.metadata.dependencies.pip_dependencies:
|
|
837
|
-
pip_install_flags = library_data.metadata.dependencies.pip_install_flags
|
|
838
|
-
if pip_install_flags is None:
|
|
839
|
-
pip_install_flags = []
|
|
840
|
-
pip_dependencies = library_data.metadata.dependencies.pip_dependencies
|
|
841
|
-
|
|
842
|
-
# Determine venv path for dependency installation
|
|
843
|
-
venv_path = self._get_library_venv_path(library_data.name, file_path)
|
|
844
|
-
|
|
845
|
-
# Only install dependencies if conditions are met
|
|
846
|
-
try:
|
|
847
|
-
library_venv_python_path = await self._init_library_venv(venv_path)
|
|
848
|
-
except RuntimeError as e:
|
|
849
|
-
self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
|
|
850
|
-
library_path=file_path,
|
|
851
|
-
library_name=library_data.name,
|
|
852
|
-
library_version=library_version,
|
|
853
|
-
status=LibraryStatus.UNUSABLE,
|
|
854
|
-
problems=[VenvCreationFailedProblem(error_message=str(e))],
|
|
855
|
-
)
|
|
856
|
-
details = f"Attempted to load Library JSON file from '{json_path}'. Failed when creating the virtual environment: {e}."
|
|
857
|
-
logger.error(details)
|
|
858
|
-
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
859
|
-
if self._can_write_to_venv_location(library_venv_python_path):
|
|
860
|
-
# Check disk space before installing dependencies
|
|
861
|
-
config_manager = GriptapeNodes.ConfigManager()
|
|
862
|
-
min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_libraries")
|
|
863
|
-
if not OSManager.check_available_disk_space(Path(venv_path), min_space_gb):
|
|
864
|
-
error_msg = OSManager.format_disk_space_error(Path(venv_path))
|
|
865
|
-
details = f"Attempted to load Library JSON from '{json_path}'. Failed when installing dependencies due to insufficient disk space (requires {min_space_gb} GB): {error_msg}"
|
|
866
|
-
logger.error(details)
|
|
867
|
-
self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
|
|
868
|
-
library_path=file_path,
|
|
869
|
-
library_name=library_data.name,
|
|
870
|
-
library_version=library_version,
|
|
871
|
-
status=LibraryStatus.UNUSABLE,
|
|
872
|
-
problems=[InsufficientDiskSpaceProblem(min_space_gb=min_space_gb, error_message=error_msg)],
|
|
873
|
-
)
|
|
874
|
-
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
875
|
-
|
|
876
|
-
# Grab the python executable from the virtual environment so that we can pip install there
|
|
877
|
-
logger.info(
|
|
878
|
-
"Installing dependencies for library '%s' with pip in venv at %s", library_data.name, venv_path
|
|
879
|
-
)
|
|
880
|
-
is_debug = config_manager.get_config_value("log_level").upper() == "DEBUG"
|
|
881
|
-
await subprocess_run(
|
|
882
|
-
[
|
|
883
|
-
sys.executable,
|
|
884
|
-
"-m",
|
|
885
|
-
"uv",
|
|
886
|
-
"pip",
|
|
887
|
-
"install",
|
|
888
|
-
*pip_dependencies,
|
|
889
|
-
*pip_install_flags,
|
|
890
|
-
"--python",
|
|
891
|
-
str(library_venv_python_path),
|
|
892
|
-
],
|
|
893
|
-
check=True,
|
|
894
|
-
capture_output=not is_debug,
|
|
895
|
-
text=True,
|
|
896
|
-
)
|
|
897
|
-
else:
|
|
898
|
-
logger.debug(
|
|
899
|
-
"Skipping dependency installation for library '%s' - venv location at %s is not writable",
|
|
900
|
-
library_data.name,
|
|
901
|
-
venv_path,
|
|
902
|
-
)
|
|
903
|
-
except subprocess.CalledProcessError as e:
|
|
904
|
-
# Failed to create the library
|
|
905
|
-
error_details = f"return code={e.returncode}, stdout={e.stdout}, stderr={e.stderr}"
|
|
906
|
-
|
|
907
|
-
self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
|
|
908
|
-
library_path=file_path,
|
|
909
|
-
library_name=library_data.name,
|
|
910
|
-
library_version=library_version,
|
|
911
|
-
status=LibraryStatus.UNUSABLE,
|
|
912
|
-
problems=[DependencyInstallationFailedProblem(error_details=error_details)],
|
|
913
|
-
)
|
|
914
|
-
details = f"Attempted to load Library JSON file from '{json_path}'. Failed when installing dependencies: {error_details}"
|
|
915
|
-
logger.error(details)
|
|
916
956
|
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
917
957
|
|
|
918
958
|
# We are at least potentially viable.
|
|
@@ -924,7 +964,10 @@ class LibraryManager:
|
|
|
924
964
|
# Assign them into the config space.
|
|
925
965
|
for library_data_setting in library_data.settings:
|
|
926
966
|
# Does the category exist?
|
|
927
|
-
get_category_request = GetConfigCategoryRequest(
|
|
967
|
+
get_category_request = GetConfigCategoryRequest(
|
|
968
|
+
category=library_data_setting.category,
|
|
969
|
+
failure_log_level=logging.DEBUG, # Missing category is expected, suppress error toast
|
|
970
|
+
)
|
|
928
971
|
get_category_result = GriptapeNodes.handle_request(get_category_request)
|
|
929
972
|
if not isinstance(get_category_result, GetConfigCategoryResultSuccess):
|
|
930
973
|
# That's OK, we'll invent it. Or at least we'll try.
|
|
@@ -980,7 +1023,6 @@ class LibraryManager:
|
|
|
980
1023
|
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
981
1024
|
case _:
|
|
982
1025
|
details = f"Attempted to load Library JSON file from '{json_path}'. Failed because an unknown/unexpected status '{library_load_results.status}' was returned."
|
|
983
|
-
logger.error(details)
|
|
984
1026
|
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
985
1027
|
|
|
986
1028
|
async def register_library_from_requirement_specifier_request(
|
|
@@ -991,21 +1033,29 @@ class LibraryManager:
|
|
|
991
1033
|
# Determine venv path for dependency installation
|
|
992
1034
|
venv_path = self._get_library_venv_path(package_name, None)
|
|
993
1035
|
|
|
1036
|
+
# Check if venv already exists before initialization
|
|
1037
|
+
venv_already_exists = venv_path.exists()
|
|
1038
|
+
|
|
994
1039
|
# Only install dependencies if conditions are met
|
|
995
1040
|
try:
|
|
996
1041
|
library_python_venv_path = await self._init_library_venv(venv_path)
|
|
997
1042
|
except RuntimeError as e:
|
|
998
1043
|
details = f"Attempted to install library '{request.requirement_specifier}'. Failed when creating the virtual environment: {e}"
|
|
999
|
-
logger.error(details)
|
|
1000
1044
|
return RegisterLibraryFromRequirementSpecifierResultFailure(result_details=details)
|
|
1001
|
-
|
|
1045
|
+
|
|
1046
|
+
if venv_already_exists:
|
|
1047
|
+
logger.debug(
|
|
1048
|
+
"Skipping dependency installation for package '%s' - venv already exists at %s",
|
|
1049
|
+
package_name,
|
|
1050
|
+
venv_path,
|
|
1051
|
+
)
|
|
1052
|
+
elif self._can_write_to_venv_location(library_python_venv_path):
|
|
1002
1053
|
# Check disk space before installing dependencies
|
|
1003
1054
|
config_manager = GriptapeNodes.ConfigManager()
|
|
1004
1055
|
min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_libraries")
|
|
1005
1056
|
if not OSManager.check_available_disk_space(Path(venv_path), min_space_gb):
|
|
1006
1057
|
error_msg = OSManager.format_disk_space_error(Path(venv_path))
|
|
1007
1058
|
details = f"Attempted to install library '{request.requirement_specifier}'. Failed when installing dependencies due to insufficient disk space (requires {min_space_gb} GB): {error_msg}"
|
|
1008
|
-
logger.error(details)
|
|
1009
1059
|
return RegisterLibraryFromRequirementSpecifierResultFailure(result_details=details)
|
|
1010
1060
|
|
|
1011
1061
|
uv_path = find_uv_bin()
|
|
@@ -1033,11 +1083,9 @@ class LibraryManager:
|
|
|
1033
1083
|
)
|
|
1034
1084
|
except subprocess.CalledProcessError as e:
|
|
1035
1085
|
details = f"Attempted to install library '{request.requirement_specifier}'. Failed: return code={e.returncode}, stdout={e.stdout}, stderr={e.stderr}"
|
|
1036
|
-
logger.error(details)
|
|
1037
1086
|
return RegisterLibraryFromRequirementSpecifierResultFailure(result_details=details)
|
|
1038
1087
|
except InvalidRequirement as e:
|
|
1039
1088
|
details = f"Attempted to install library '{request.requirement_specifier}'. Failed due to invalid requirement specifier: {e}"
|
|
1040
|
-
logger.error(details)
|
|
1041
1089
|
return RegisterLibraryFromRequirementSpecifierResultFailure(result_details=details)
|
|
1042
1090
|
|
|
1043
1091
|
library_path = str(files(package_name).joinpath(request.library_config_name))
|
|
@@ -1045,7 +1093,6 @@ class LibraryManager:
|
|
|
1045
1093
|
register_result = GriptapeNodes.handle_request(RegisterLibraryFromFileRequest(file_path=library_path))
|
|
1046
1094
|
if isinstance(register_result, RegisterLibraryFromFileResultFailure):
|
|
1047
1095
|
details = f"Attempted to install library '{request.requirement_specifier}'. Failed due to {register_result}"
|
|
1048
|
-
logger.error(details)
|
|
1049
1096
|
return RegisterLibraryFromRequirementSpecifierResultFailure(result_details=details)
|
|
1050
1097
|
|
|
1051
1098
|
return RegisterLibraryFromRequirementSpecifierResultSuccess(
|
|
@@ -1107,17 +1154,6 @@ class LibraryManager:
|
|
|
1107
1154
|
else:
|
|
1108
1155
|
library_venv_python_path = library_venv_path / "bin" / "python"
|
|
1109
1156
|
|
|
1110
|
-
# Need to insert into the path so that the library picks up on the venv
|
|
1111
|
-
site_packages = str(
|
|
1112
|
-
Path(
|
|
1113
|
-
sysconfig.get_path(
|
|
1114
|
-
"purelib",
|
|
1115
|
-
vars={"base": str(library_venv_path), "platbase": str(library_venv_path)},
|
|
1116
|
-
)
|
|
1117
|
-
)
|
|
1118
|
-
)
|
|
1119
|
-
sys.path.insert(0, site_packages)
|
|
1120
|
-
|
|
1121
1157
|
return library_venv_python_path
|
|
1122
1158
|
|
|
1123
1159
|
def _get_library_venv_path(self, library_name: str, library_file_path: str | None = None) -> Path:
|
|
@@ -1176,7 +1212,6 @@ class LibraryManager:
|
|
|
1176
1212
|
LibraryRegistry.unregister_library(library_name=request.library_name)
|
|
1177
1213
|
except Exception as e:
|
|
1178
1214
|
details = f"Attempted to unload library '{request.library_name}'. Failed due to {e}"
|
|
1179
|
-
logger.error(details)
|
|
1180
1215
|
return UnloadLibraryFromRegistryResultFailure(result_details=details)
|
|
1181
1216
|
|
|
1182
1217
|
# Clean up all stable module aliases for this library
|
|
@@ -1196,7 +1231,6 @@ class LibraryManager:
|
|
|
1196
1231
|
|
|
1197
1232
|
if not list_libraries_result.succeeded():
|
|
1198
1233
|
details = "Attempted to get all info for all libraries, but listing the registered libraries failed."
|
|
1199
|
-
logger.error(details)
|
|
1200
1234
|
return GetAllInfoForAllLibrariesResultFailure(result_details=details)
|
|
1201
1235
|
|
|
1202
1236
|
try:
|
|
@@ -1211,7 +1245,6 @@ class LibraryManager:
|
|
|
1211
1245
|
|
|
1212
1246
|
if not library_all_info_result.succeeded():
|
|
1213
1247
|
details = f"Attempted to get all info for all libraries, but failed when getting all info for library named '{library_name}'."
|
|
1214
|
-
logger.error(details)
|
|
1215
1248
|
return GetAllInfoForAllLibrariesResultFailure(result_details=details)
|
|
1216
1249
|
|
|
1217
1250
|
library_all_info_success = cast("GetAllInfoForLibraryResultSuccess", library_all_info_result)
|
|
@@ -1219,7 +1252,6 @@ class LibraryManager:
|
|
|
1219
1252
|
library_name_to_all_info[library_name] = library_all_info_success
|
|
1220
1253
|
except Exception as err:
|
|
1221
1254
|
details = f"Attempted to get all info for all libraries. Encountered error {err}."
|
|
1222
|
-
logger.error(details)
|
|
1223
1255
|
return GetAllInfoForAllLibrariesResultFailure(result_details=details)
|
|
1224
1256
|
|
|
1225
1257
|
# We're home free now
|
|
@@ -1242,7 +1274,6 @@ class LibraryManager:
|
|
|
1242
1274
|
LibraryRegistry.get_library(name=request.library)
|
|
1243
1275
|
except KeyError:
|
|
1244
1276
|
details = f"Attempted to get all library info for a Library named '{request.library}'. Failed because no Library with that name was registered."
|
|
1245
|
-
logger.error(details)
|
|
1246
1277
|
result = GetAllInfoForLibraryResultFailure(result_details=details)
|
|
1247
1278
|
return result
|
|
1248
1279
|
|
|
@@ -1251,7 +1282,6 @@ class LibraryManager:
|
|
|
1251
1282
|
|
|
1252
1283
|
if not library_metadata_result.succeeded():
|
|
1253
1284
|
details = f"Attempted to get all library info for a Library named '{request.library}'. Failed attempting to get the library's metadata."
|
|
1254
|
-
logger.error(details)
|
|
1255
1285
|
return GetAllInfoForLibraryResultFailure(result_details=details)
|
|
1256
1286
|
|
|
1257
1287
|
list_categories_request = ListCategoriesInLibraryRequest(library=request.library)
|
|
@@ -1259,7 +1289,6 @@ class LibraryManager:
|
|
|
1259
1289
|
|
|
1260
1290
|
if not list_categories_result.succeeded():
|
|
1261
1291
|
details = f"Attempted to get all library info for a Library named '{request.library}'. Failed attempting to get the list of categories in the library."
|
|
1262
|
-
logger.error(details)
|
|
1263
1292
|
return GetAllInfoForLibraryResultFailure(result_details=details)
|
|
1264
1293
|
|
|
1265
1294
|
node_type_list_request = ListNodeTypesInLibraryRequest(library=request.library)
|
|
@@ -1267,7 +1296,6 @@ class LibraryManager:
|
|
|
1267
1296
|
|
|
1268
1297
|
if not node_type_list_result.succeeded():
|
|
1269
1298
|
details = f"Attempted to get all library info for a Library named '{request.library}'. Failed attempting to get the list of node types in the library."
|
|
1270
|
-
logger.error(details)
|
|
1271
1299
|
return GetAllInfoForLibraryResultFailure(result_details=details)
|
|
1272
1300
|
|
|
1273
1301
|
# Cast everyone to their success counterparts.
|
|
@@ -1279,7 +1307,6 @@ class LibraryManager:
|
|
|
1279
1307
|
details = (
|
|
1280
1308
|
f"Attempted to get all library info for a Library named '{request.library}'. Encountered error: {err}."
|
|
1281
1309
|
)
|
|
1282
|
-
logger.error(details)
|
|
1283
1310
|
return GetAllInfoForLibraryResultFailure(result_details=details)
|
|
1284
1311
|
|
|
1285
1312
|
# Now build the map of node types to metadata.
|
|
@@ -1290,14 +1317,12 @@ class LibraryManager:
|
|
|
1290
1317
|
|
|
1291
1318
|
if not node_metadata_result.succeeded():
|
|
1292
1319
|
details = f"Attempted to get all library info for a Library named '{request.library}'. Failed attempting to get the metadata for a node type called '{node_type_name}'."
|
|
1293
|
-
logger.error(details)
|
|
1294
1320
|
return GetAllInfoForLibraryResultFailure(result_details=details)
|
|
1295
1321
|
|
|
1296
1322
|
try:
|
|
1297
1323
|
node_metadata_result_success = cast("GetNodeMetadataFromLibraryResultSuccess", node_metadata_result)
|
|
1298
1324
|
except Exception as err:
|
|
1299
1325
|
details = f"Attempted to get all library info for a Library named '{request.library}'. Encountered error: {err}."
|
|
1300
|
-
logger.error(details)
|
|
1301
1326
|
return GetAllInfoForLibraryResultFailure(result_details=details)
|
|
1302
1327
|
|
|
1303
1328
|
# Put it into the map.
|
|
@@ -1686,13 +1711,132 @@ class LibraryManager:
|
|
|
1686
1711
|
self.print_library_load_status()
|
|
1687
1712
|
|
|
1688
1713
|
# Remove any missing libraries AFTER we've printed them for the user.
|
|
1689
|
-
user_libraries_section =
|
|
1714
|
+
user_libraries_section = LIBRARIES_TO_REGISTER_KEY
|
|
1690
1715
|
self._remove_missing_libraries_from_config(config_category=user_libraries_section)
|
|
1691
1716
|
finally:
|
|
1692
1717
|
self._libraries_loading_complete.set()
|
|
1693
1718
|
|
|
1719
|
+
async def _ensure_libraries_from_config(self) -> None:
|
|
1720
|
+
"""Ensure libraries from git URLs specified in config are downloaded.
|
|
1721
|
+
|
|
1722
|
+
This method:
|
|
1723
|
+
1. Reads libraries_to_download from config
|
|
1724
|
+
2. Downloads any missing libraries concurrently
|
|
1725
|
+
3. Logs summary of successful/failed operations
|
|
1726
|
+
|
|
1727
|
+
Supports URL format with @ref suffix (e.g., "https://github.com/user/repo@stable").
|
|
1728
|
+
Libraries are registered later by load_all_libraries_from_config().
|
|
1729
|
+
"""
|
|
1730
|
+
config_mgr = GriptapeNodes.ConfigManager()
|
|
1731
|
+
git_urls = config_mgr.get_config_value(LIBRARIES_TO_DOWNLOAD_KEY, default=[])
|
|
1732
|
+
|
|
1733
|
+
if not git_urls:
|
|
1734
|
+
logger.debug("No libraries to download from config")
|
|
1735
|
+
return
|
|
1736
|
+
|
|
1737
|
+
logger.info("Starting download of %d libraries from config", len(git_urls))
|
|
1738
|
+
|
|
1739
|
+
# Use shared download method
|
|
1740
|
+
results = await self._download_libraries_from_git_urls(git_urls)
|
|
1741
|
+
|
|
1742
|
+
# Count successes and failures
|
|
1743
|
+
successful = sum(1 for r in results.values() if r["success"])
|
|
1744
|
+
failed = len(results) - successful
|
|
1745
|
+
|
|
1746
|
+
logger.info(
|
|
1747
|
+
"Completed automatic library downloads: %d successful, %d failed",
|
|
1748
|
+
successful,
|
|
1749
|
+
failed,
|
|
1750
|
+
)
|
|
1751
|
+
|
|
1752
|
+
async def _download_libraries_from_git_urls(
|
|
1753
|
+
self,
|
|
1754
|
+
git_urls_with_refs: list[str],
|
|
1755
|
+
) -> dict[str, dict[str, Any]]:
|
|
1756
|
+
"""Download multiple libraries from git URLs concurrently.
|
|
1757
|
+
|
|
1758
|
+
Args:
|
|
1759
|
+
git_urls_with_refs: List of git URLs with optional @ref suffix (e.g., "url@v1.0")
|
|
1760
|
+
|
|
1761
|
+
Returns:
|
|
1762
|
+
Dictionary mapping git_url_with_ref to result info:
|
|
1763
|
+
{
|
|
1764
|
+
"url@ref": {
|
|
1765
|
+
"success": bool,
|
|
1766
|
+
"library_name": str | None,
|
|
1767
|
+
"error": str | None,
|
|
1768
|
+
"skipped": bool (optional, True if already exists),
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
"""
|
|
1772
|
+
config_mgr = GriptapeNodes.ConfigManager()
|
|
1773
|
+
libraries_dir_setting = config_mgr.get_config_value("libraries_directory")
|
|
1774
|
+
|
|
1775
|
+
if not libraries_dir_setting:
|
|
1776
|
+
logger.warning("Cannot download libraries: libraries_directory not configured")
|
|
1777
|
+
return {}
|
|
1778
|
+
|
|
1779
|
+
libraries_path = config_mgr.workspace_path / libraries_dir_setting
|
|
1780
|
+
|
|
1781
|
+
async def download_one(git_url_with_ref: str) -> tuple[str, dict[str, Any]]:
|
|
1782
|
+
"""Download a single library if not already present."""
|
|
1783
|
+
# Parse URL to extract git URL and optional ref
|
|
1784
|
+
git_url, ref = parse_git_url_with_ref(git_url_with_ref)
|
|
1785
|
+
target_directory_name = extract_repo_name_from_url(git_url)
|
|
1786
|
+
target_path = libraries_path / target_directory_name
|
|
1787
|
+
|
|
1788
|
+
# Skip if already exists
|
|
1789
|
+
if target_path.exists():
|
|
1790
|
+
logger.info("Library at '%s' already exists, skipping", target_path)
|
|
1791
|
+
return git_url_with_ref, {
|
|
1792
|
+
"success": False,
|
|
1793
|
+
"library_name": None,
|
|
1794
|
+
"error": None,
|
|
1795
|
+
"skipped": True,
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
logger.info("Downloading library from '%s'", git_url_with_ref)
|
|
1799
|
+
download_result = await GriptapeNodes.ahandle_request(
|
|
1800
|
+
DownloadLibraryRequest(
|
|
1801
|
+
git_url=git_url,
|
|
1802
|
+
branch_tag_commit=ref,
|
|
1803
|
+
fail_on_exists=False,
|
|
1804
|
+
auto_register=False,
|
|
1805
|
+
)
|
|
1806
|
+
)
|
|
1807
|
+
|
|
1808
|
+
if isinstance(download_result, DownloadLibraryResultSuccess):
|
|
1809
|
+
logger.info("Downloaded library '%s'", download_result.library_name)
|
|
1810
|
+
return git_url_with_ref, {
|
|
1811
|
+
"success": True,
|
|
1812
|
+
"library_name": download_result.library_name,
|
|
1813
|
+
"error": None,
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
error = str(download_result.result_details)
|
|
1817
|
+
logger.warning("Failed to download '%s': %s", git_url_with_ref, error)
|
|
1818
|
+
return git_url_with_ref, {
|
|
1819
|
+
"success": False,
|
|
1820
|
+
"library_name": None,
|
|
1821
|
+
"error": error,
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
# Download all concurrently
|
|
1825
|
+
async with asyncio.TaskGroup() as tg:
|
|
1826
|
+
tasks = [tg.create_task(download_one(url)) for url in git_urls_with_refs]
|
|
1827
|
+
|
|
1828
|
+
# Collect results
|
|
1829
|
+
return dict(task.result() for task in tasks)
|
|
1830
|
+
|
|
1694
1831
|
async def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
|
|
1695
|
-
#
|
|
1832
|
+
# Automatically migrate old XDG library paths from config
|
|
1833
|
+
# TODO: Remove https://github.com/griptape-ai/griptape-nodes/issues/3348
|
|
1834
|
+
self._migrate_old_xdg_library_paths()
|
|
1835
|
+
|
|
1836
|
+
# App just got init'd. First download any missing libraries from git URLs.
|
|
1837
|
+
await self._ensure_libraries_from_config()
|
|
1838
|
+
|
|
1839
|
+
# Now load all libraries from config (including newly downloaded ones)
|
|
1696
1840
|
await self.load_all_libraries_from_config()
|
|
1697
1841
|
|
|
1698
1842
|
# Register all secrets now that libraries are loaded and settings are merged
|
|
@@ -1771,7 +1915,7 @@ class LibraryManager:
|
|
|
1771
1915
|
config_mgr = GriptapeNodes.ConfigManager()
|
|
1772
1916
|
|
|
1773
1917
|
# Get the current libraries_to_register list
|
|
1774
|
-
user_libraries_section =
|
|
1918
|
+
user_libraries_section = LIBRARIES_TO_REGISTER_KEY
|
|
1775
1919
|
libraries_to_register: list[str] = config_mgr.get_config_value(user_libraries_section)
|
|
1776
1920
|
|
|
1777
1921
|
# Filter out empty or whitespace-only entries
|
|
@@ -1926,6 +2070,25 @@ class LibraryManager:
|
|
|
1926
2070
|
if issue.severity == LibraryStatus.UNUSABLE:
|
|
1927
2071
|
has_disqualifying_issues = True
|
|
1928
2072
|
|
|
2073
|
+
# Check if library is in old XDG location
|
|
2074
|
+
old_xdg_libraries_path = xdg_data_home() / "griptape_nodes" / "libraries"
|
|
2075
|
+
library_path_obj = Path(library_file_path)
|
|
2076
|
+
try:
|
|
2077
|
+
# Check if the library path is relative to the old XDG location
|
|
2078
|
+
if library_path_obj.is_relative_to(old_xdg_libraries_path):
|
|
2079
|
+
problems.append(OldXdgLocationWarningProblem(old_path=str(library_path_obj)))
|
|
2080
|
+
logger.warning(
|
|
2081
|
+
"Library '%s' is located in old XDG data directory: %s. "
|
|
2082
|
+
"Starting with version 0.65.0, libraries are managed in your workspace directory. "
|
|
2083
|
+
"To migrate: run 'gtn init' (CLI) or go to App Settings and click 'Re-run Setup Wizard' (desktop app).",
|
|
2084
|
+
library_data.name,
|
|
2085
|
+
library_file_path,
|
|
2086
|
+
)
|
|
2087
|
+
except ValueError:
|
|
2088
|
+
# is_relative_to() raises ValueError if paths are on different drives
|
|
2089
|
+
# In this case, library is definitely not in the old XDG location
|
|
2090
|
+
pass
|
|
2091
|
+
|
|
1929
2092
|
# Early exit if any version issues are disqualifying
|
|
1930
2093
|
if has_disqualifying_issues:
|
|
1931
2094
|
return LibraryManager.LibraryInfo(
|
|
@@ -2136,8 +2299,13 @@ class LibraryManager:
|
|
|
2136
2299
|
self._library_file_path_to_info[sandbox_library_dir_as_posix] = library_load_results
|
|
2137
2300
|
|
|
2138
2301
|
def _find_files_in_dir(self, directory: Path, extension: str) -> list[Path]:
|
|
2302
|
+
"""Find all files with given extension in directory, excluding common non-source directories."""
|
|
2139
2303
|
ret_val = []
|
|
2140
|
-
for root,
|
|
2304
|
+
for root, dirs, files_found in os.walk(directory):
|
|
2305
|
+
# Modify dirs in-place to skip excluded directories
|
|
2306
|
+
# Also skip any directory starting with '.'
|
|
2307
|
+
dirs[:] = [d for d in dirs if d not in EXCLUDED_SCAN_DIRECTORIES and not d.startswith(".")]
|
|
2308
|
+
|
|
2141
2309
|
for file in files_found:
|
|
2142
2310
|
if file.endswith(extension):
|
|
2143
2311
|
file_path = Path(root) / file
|
|
@@ -2161,13 +2329,94 @@ class LibraryManager:
|
|
|
2161
2329
|
]
|
|
2162
2330
|
config_mgr.set_config_value(config_category, libraries_to_register_category)
|
|
2163
2331
|
|
|
2332
|
+
def _migrate_old_xdg_library_paths(self) -> None:
|
|
2333
|
+
"""Automatically removes old XDG library paths and adds git URLs to download list.
|
|
2334
|
+
|
|
2335
|
+
This method removes library paths that were stored in the old XDG data home location
|
|
2336
|
+
(~/.local/share/griptape_nodes/libraries/) from the libraries_to_register configuration,
|
|
2337
|
+
and automatically adds the corresponding git URLs to libraries_to_download to ensure
|
|
2338
|
+
the libraries are re-downloaded. This migration happens automatically on app startup,
|
|
2339
|
+
so users don't need to run gtn init.
|
|
2340
|
+
"""
|
|
2341
|
+
config_mgr = GriptapeNodes.ConfigManager()
|
|
2342
|
+
|
|
2343
|
+
# Get both config lists
|
|
2344
|
+
register_key = LIBRARIES_TO_REGISTER_KEY
|
|
2345
|
+
download_key = LIBRARIES_TO_DOWNLOAD_KEY
|
|
2346
|
+
|
|
2347
|
+
libraries_to_register = config_mgr.get_config_value(register_key)
|
|
2348
|
+
libraries_to_download = config_mgr.get_config_value(download_key) or []
|
|
2349
|
+
|
|
2350
|
+
if not libraries_to_register:
|
|
2351
|
+
return
|
|
2352
|
+
|
|
2353
|
+
# Filter and get which libraries were removed
|
|
2354
|
+
filtered_libraries, removed_library_names = filter_old_xdg_library_paths(libraries_to_register)
|
|
2355
|
+
|
|
2356
|
+
# If any paths were removed
|
|
2357
|
+
paths_removed = len(libraries_to_register) - len(filtered_libraries)
|
|
2358
|
+
if paths_removed > 0:
|
|
2359
|
+
# Update libraries_to_register
|
|
2360
|
+
config_mgr.set_config_value(register_key, filtered_libraries)
|
|
2361
|
+
|
|
2362
|
+
# Add corresponding git URLs to libraries_to_download
|
|
2363
|
+
updated_downloads = self._add_git_urls_for_removed_libraries(
|
|
2364
|
+
libraries_to_download,
|
|
2365
|
+
removed_library_names,
|
|
2366
|
+
)
|
|
2367
|
+
|
|
2368
|
+
urls_added = len(updated_downloads) - len(libraries_to_download)
|
|
2369
|
+
if urls_added > 0:
|
|
2370
|
+
config_mgr.set_config_value(download_key, updated_downloads)
|
|
2371
|
+
|
|
2372
|
+
logger.info(
|
|
2373
|
+
"Automatically migrated library configuration: removed %d old XDG path(s), added %d git URL(s) to download",
|
|
2374
|
+
paths_removed,
|
|
2375
|
+
urls_added,
|
|
2376
|
+
)
|
|
2377
|
+
|
|
2378
|
+
def _add_git_urls_for_removed_libraries(
|
|
2379
|
+
self,
|
|
2380
|
+
current_downloads: list[str],
|
|
2381
|
+
removed_library_names: set[str],
|
|
2382
|
+
) -> list[str]:
|
|
2383
|
+
"""Add git URLs for removed libraries if not already present.
|
|
2384
|
+
|
|
2385
|
+
Args:
|
|
2386
|
+
current_downloads: Current list of git URLs in libraries_to_download
|
|
2387
|
+
removed_library_names: Set of library names that were removed (e.g., "griptape_nodes_library")
|
|
2388
|
+
|
|
2389
|
+
Returns:
|
|
2390
|
+
Updated list with new git URLs added (deduplicated)
|
|
2391
|
+
"""
|
|
2392
|
+
if not removed_library_names:
|
|
2393
|
+
return current_downloads
|
|
2394
|
+
|
|
2395
|
+
# Get current repository names for deduplication
|
|
2396
|
+
current_repo_names = {extract_repo_name_from_url(url) for url in current_downloads}
|
|
2397
|
+
|
|
2398
|
+
new_downloads = current_downloads.copy()
|
|
2399
|
+
|
|
2400
|
+
for lib_name in removed_library_names:
|
|
2401
|
+
if lib_name not in LIBRARY_GIT_URLS:
|
|
2402
|
+
continue
|
|
2403
|
+
|
|
2404
|
+
git_url = LIBRARY_GIT_URLS[lib_name]
|
|
2405
|
+
repo_name = extract_repo_name_from_url(git_url)
|
|
2406
|
+
|
|
2407
|
+
# Only add if not already present
|
|
2408
|
+
if repo_name not in current_repo_names:
|
|
2409
|
+
new_downloads.append(git_url)
|
|
2410
|
+
current_repo_names.add(repo_name)
|
|
2411
|
+
|
|
2412
|
+
return new_downloads
|
|
2413
|
+
|
|
2164
2414
|
async def reload_libraries_request(self, request: ReloadAllLibrariesRequest) -> ResultPayload: # noqa: ARG002
|
|
2165
2415
|
# Start with a clean slate.
|
|
2166
2416
|
clear_all_request = ClearAllObjectStateRequest(i_know_what_im_doing=True)
|
|
2167
2417
|
clear_all_result = await GriptapeNodes.ahandle_request(clear_all_request)
|
|
2168
2418
|
if not clear_all_result.succeeded():
|
|
2169
2419
|
details = "Failed to clear the existing object state when preparing to reload all libraries."
|
|
2170
|
-
logger.error(details)
|
|
2171
2420
|
return ReloadAllLibrariesResultFailure(result_details=details)
|
|
2172
2421
|
|
|
2173
2422
|
# Unload all libraries now.
|
|
@@ -2220,7 +2469,7 @@ class LibraryManager:
|
|
|
2220
2469
|
List of library file paths found
|
|
2221
2470
|
"""
|
|
2222
2471
|
config_mgr = GriptapeNodes.ConfigManager()
|
|
2223
|
-
user_libraries_section =
|
|
2472
|
+
user_libraries_section = LIBRARIES_TO_REGISTER_KEY
|
|
2224
2473
|
|
|
2225
2474
|
discovered_libraries = set()
|
|
2226
2475
|
|
|
@@ -2242,3 +2491,758 @@ class LibraryManager:
|
|
|
2242
2491
|
process_path(library_path)
|
|
2243
2492
|
|
|
2244
2493
|
return list(discovered_libraries)
|
|
2494
|
+
|
|
2495
|
+
async def check_library_update_request(self, request: CheckLibraryUpdateRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912, PLR0915
|
|
2496
|
+
"""Check if a library has updates available via git."""
|
|
2497
|
+
library_name = request.library_name
|
|
2498
|
+
|
|
2499
|
+
# Check if the library exists
|
|
2500
|
+
try:
|
|
2501
|
+
library = LibraryRegistry.get_library(name=library_name)
|
|
2502
|
+
except KeyError:
|
|
2503
|
+
details = f"Attempted to check for updates for Library '{library_name}'. Failed because no Library with that name was registered."
|
|
2504
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2505
|
+
|
|
2506
|
+
# Find the library file path
|
|
2507
|
+
library_file_path = None
|
|
2508
|
+
for file_path, library_info in self._library_file_path_to_info.items():
|
|
2509
|
+
if library_info.library_name == library_name:
|
|
2510
|
+
library_file_path = file_path
|
|
2511
|
+
break
|
|
2512
|
+
|
|
2513
|
+
if library_file_path is None:
|
|
2514
|
+
details = f"Attempted to check for updates for Library '{library_name}'. Failed because no file path could be found for this library."
|
|
2515
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2516
|
+
|
|
2517
|
+
# Get the library directory (parent of the JSON file)
|
|
2518
|
+
library_dir = Path(library_file_path).parent.absolute()
|
|
2519
|
+
|
|
2520
|
+
# Check if library is in a monorepo (multiple libraries in same git repository)
|
|
2521
|
+
if await asyncio.to_thread(is_monorepo, library_dir):
|
|
2522
|
+
details = (
|
|
2523
|
+
f"Library '{library_name}' is in a monorepo with multiple libraries. Updates must be managed manually."
|
|
2524
|
+
)
|
|
2525
|
+
logger.info(details)
|
|
2526
|
+
# Get git info for the response
|
|
2527
|
+
git_remote = await asyncio.to_thread(get_git_remote, library_dir)
|
|
2528
|
+
git_ref = await asyncio.to_thread(get_current_ref, library_dir)
|
|
2529
|
+
current_version = library.get_metadata().library_version
|
|
2530
|
+
return CheckLibraryUpdateResultSuccess(
|
|
2531
|
+
has_update=False,
|
|
2532
|
+
current_version=current_version,
|
|
2533
|
+
latest_version=current_version,
|
|
2534
|
+
git_remote=git_remote,
|
|
2535
|
+
git_ref=git_ref,
|
|
2536
|
+
local_commit=None,
|
|
2537
|
+
remote_commit=None,
|
|
2538
|
+
result_details=details,
|
|
2539
|
+
)
|
|
2540
|
+
|
|
2541
|
+
# Check if the library directory is a git repository and get remote URL and ref
|
|
2542
|
+
try:
|
|
2543
|
+
git_remote = await asyncio.to_thread(get_git_remote, library_dir)
|
|
2544
|
+
if git_remote is None:
|
|
2545
|
+
details = f"Library '{library_name}' is not a git repository or has no remote configured."
|
|
2546
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2547
|
+
except GitRemoteError as e:
|
|
2548
|
+
details = f"Failed to get git remote for Library '{library_name}': {e}"
|
|
2549
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2550
|
+
|
|
2551
|
+
try:
|
|
2552
|
+
git_ref = await asyncio.to_thread(get_current_ref, library_dir)
|
|
2553
|
+
except GitRefError as e:
|
|
2554
|
+
details = f"Failed to get current git reference for Library '{library_name}': {e}"
|
|
2555
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2556
|
+
|
|
2557
|
+
# Get current library version
|
|
2558
|
+
current_version = library.get_metadata().library_version
|
|
2559
|
+
if current_version is None:
|
|
2560
|
+
details = f"Library '{library_name}' has no version information."
|
|
2561
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2562
|
+
|
|
2563
|
+
# Get local commit SHA
|
|
2564
|
+
local_commit = await asyncio.to_thread(get_local_commit_sha, library_dir)
|
|
2565
|
+
|
|
2566
|
+
# Clone remote and get latest version and commit SHA (using current ref or HEAD if detached)
|
|
2567
|
+
try:
|
|
2568
|
+
ref_to_check = git_ref or "HEAD"
|
|
2569
|
+
version_info = await asyncio.to_thread(clone_and_get_library_version, git_remote, ref_to_check)
|
|
2570
|
+
latest_version = version_info.library_version
|
|
2571
|
+
remote_commit = version_info.commit_sha
|
|
2572
|
+
except GitCloneError as e:
|
|
2573
|
+
details = f"Failed to retrieve latest version from git remote for Library '{library_name}': {e}"
|
|
2574
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2575
|
+
|
|
2576
|
+
# Determine if update is available using version comparison and commit comparison
|
|
2577
|
+
try:
|
|
2578
|
+
current_ver = Version.parse(current_version)
|
|
2579
|
+
latest_ver = Version.parse(latest_version)
|
|
2580
|
+
|
|
2581
|
+
# Update detection logic:
|
|
2582
|
+
# 1. If remote version > local version -> update available (semantic versioning)
|
|
2583
|
+
if latest_ver > current_ver:
|
|
2584
|
+
has_update = True
|
|
2585
|
+
update_reason = "version increased"
|
|
2586
|
+
# 2. If remote version < local version -> no update (prevent regression)
|
|
2587
|
+
elif latest_ver < current_ver:
|
|
2588
|
+
has_update = False
|
|
2589
|
+
update_reason = "version decreased (regression blocked)"
|
|
2590
|
+
# 3. If versions equal -> check commits
|
|
2591
|
+
elif local_commit is not None and remote_commit is not None and local_commit != remote_commit:
|
|
2592
|
+
has_update = True
|
|
2593
|
+
update_reason = "commits differ (same version)"
|
|
2594
|
+
else:
|
|
2595
|
+
has_update = False
|
|
2596
|
+
update_reason = "versions and commits match"
|
|
2597
|
+
|
|
2598
|
+
except ValueError as e:
|
|
2599
|
+
details = f"Failed to parse version strings for Library '{library_name}': {e}"
|
|
2600
|
+
return CheckLibraryUpdateResultFailure(result_details=details)
|
|
2601
|
+
|
|
2602
|
+
details = f"Successfully checked for updates for Library '{library_name}'. Current version: {current_version}, Latest version: {latest_version}, Has update: {has_update} ({update_reason})"
|
|
2603
|
+
logger.info(details)
|
|
2604
|
+
|
|
2605
|
+
return CheckLibraryUpdateResultSuccess(
|
|
2606
|
+
has_update=has_update,
|
|
2607
|
+
current_version=current_version,
|
|
2608
|
+
latest_version=latest_version,
|
|
2609
|
+
git_remote=git_remote,
|
|
2610
|
+
git_ref=git_ref,
|
|
2611
|
+
local_commit=local_commit,
|
|
2612
|
+
remote_commit=remote_commit,
|
|
2613
|
+
result_details=details,
|
|
2614
|
+
)
|
|
2615
|
+
|
|
2616
|
+
async def _validate_and_prepare_library_for_git_operation(
|
|
2617
|
+
self,
|
|
2618
|
+
library_name: str,
|
|
2619
|
+
failure_result_class: type[ResultPayloadFailure],
|
|
2620
|
+
operation_description: str,
|
|
2621
|
+
) -> LibraryGitOperationContext | ResultPayloadFailure:
|
|
2622
|
+
"""Validate library exists and prepare for git operation.
|
|
2623
|
+
|
|
2624
|
+
Args:
|
|
2625
|
+
library_name: Name of the library to validate
|
|
2626
|
+
failure_result_class: Class to use for failure results (e.g., UpdateLibraryResultFailure)
|
|
2627
|
+
operation_description: Description of operation for error messages (e.g., "update", "switch branch/tag for")
|
|
2628
|
+
|
|
2629
|
+
Returns:
|
|
2630
|
+
On success: LibraryGitOperationContext with library info
|
|
2631
|
+
On failure: ResultPayloadFailure instance
|
|
2632
|
+
"""
|
|
2633
|
+
# Check if the library exists
|
|
2634
|
+
try:
|
|
2635
|
+
library = LibraryRegistry.get_library(name=library_name)
|
|
2636
|
+
except KeyError:
|
|
2637
|
+
details = f"Attempted to {operation_description} Library '{library_name}'. Failed because no Library with that name was registered."
|
|
2638
|
+
return failure_result_class(result_details=details)
|
|
2639
|
+
|
|
2640
|
+
# Get current version
|
|
2641
|
+
old_version = library.get_metadata().library_version
|
|
2642
|
+
if old_version is None:
|
|
2643
|
+
details = f"Library '{library_name}' has no version information."
|
|
2644
|
+
return failure_result_class(result_details=details)
|
|
2645
|
+
|
|
2646
|
+
# Find the library file path
|
|
2647
|
+
library_file_path = None
|
|
2648
|
+
for file_path, library_info in self._library_file_path_to_info.items():
|
|
2649
|
+
if library_info.library_name == library_name:
|
|
2650
|
+
library_file_path = file_path
|
|
2651
|
+
break
|
|
2652
|
+
|
|
2653
|
+
if library_file_path is None:
|
|
2654
|
+
details = f"Attempted to {operation_description} Library '{library_name}'. Failed because no file path could be found for this library."
|
|
2655
|
+
return failure_result_class(result_details=details)
|
|
2656
|
+
|
|
2657
|
+
# Get the library directory (parent of the JSON file)
|
|
2658
|
+
library_dir = Path(library_file_path).parent.absolute()
|
|
2659
|
+
|
|
2660
|
+
return LibraryGitOperationContext(
|
|
2661
|
+
library=library,
|
|
2662
|
+
old_version=old_version,
|
|
2663
|
+
library_file_path=library_file_path,
|
|
2664
|
+
library_dir=library_dir,
|
|
2665
|
+
)
|
|
2666
|
+
|
|
2667
|
+
async def _reload_library_after_git_operation(
|
|
2668
|
+
self,
|
|
2669
|
+
library_name: str,
|
|
2670
|
+
library_file_path: str,
|
|
2671
|
+
*,
|
|
2672
|
+
failure_result_class: type[ResultPayloadFailure],
|
|
2673
|
+
) -> str | ResultPayloadFailure:
|
|
2674
|
+
"""Reload library after git operation.
|
|
2675
|
+
|
|
2676
|
+
Args:
|
|
2677
|
+
library_name: Name of the library to reload
|
|
2678
|
+
library_file_path: Path to the library JSON file
|
|
2679
|
+
failure_result_class: Class to use for failure results
|
|
2680
|
+
|
|
2681
|
+
Returns:
|
|
2682
|
+
On success: new_version (str, may be "unknown")
|
|
2683
|
+
On failure: ResultPayloadFailure instance
|
|
2684
|
+
"""
|
|
2685
|
+
# Unload the library
|
|
2686
|
+
unload_result = GriptapeNodes.handle_request(UnloadLibraryFromRegistryRequest(library_name=library_name))
|
|
2687
|
+
if not unload_result.succeeded():
|
|
2688
|
+
details = f"Failed to unload Library '{library_name}' after git operation."
|
|
2689
|
+
return failure_result_class(result_details=details)
|
|
2690
|
+
|
|
2691
|
+
# Search for the library JSON file using flexible pattern to handle filename variations
|
|
2692
|
+
# (after git operations, the filename might change between griptape-nodes-library.json and griptape_nodes_library.json)
|
|
2693
|
+
library_dir = Path(library_file_path).parent
|
|
2694
|
+
actual_library_file = find_file_in_directory(library_dir, "griptape[-_]nodes[-_]library.json")
|
|
2695
|
+
|
|
2696
|
+
if actual_library_file is None:
|
|
2697
|
+
details = (
|
|
2698
|
+
f"Failed to find library JSON file in {library_dir} after git operation for Library '{library_name}'."
|
|
2699
|
+
)
|
|
2700
|
+
return failure_result_class(result_details=details)
|
|
2701
|
+
|
|
2702
|
+
# Use the found file path for reloading
|
|
2703
|
+
actual_library_file_path = str(actual_library_file)
|
|
2704
|
+
|
|
2705
|
+
# Reload the library from file
|
|
2706
|
+
reload_result = await GriptapeNodes.ahandle_request(
|
|
2707
|
+
RegisterLibraryFromFileRequest(file_path=actual_library_file_path)
|
|
2708
|
+
)
|
|
2709
|
+
if not isinstance(reload_result, RegisterLibraryFromFileResultSuccess):
|
|
2710
|
+
details = f"Failed to reload Library '{library_name}' after git operation."
|
|
2711
|
+
return failure_result_class(result_details=details)
|
|
2712
|
+
|
|
2713
|
+
# Get new version after reload
|
|
2714
|
+
try:
|
|
2715
|
+
updated_library = LibraryRegistry.get_library(name=library_name)
|
|
2716
|
+
new_version = updated_library.get_metadata().library_version
|
|
2717
|
+
if new_version is None:
|
|
2718
|
+
new_version = "unknown"
|
|
2719
|
+
except KeyError:
|
|
2720
|
+
new_version = "unknown"
|
|
2721
|
+
|
|
2722
|
+
return new_version
|
|
2723
|
+
|
|
2724
|
+
async def update_library_request(self, request: UpdateLibraryRequest) -> ResultPayload:
|
|
2725
|
+
"""Update a library to the latest version using the appropriate git strategy.
|
|
2726
|
+
|
|
2727
|
+
Automatically detects whether the library uses branch-based or tag-based workflow:
|
|
2728
|
+
- Branch-based: Uses git fetch + git reset --hard (forces local to match remote)
|
|
2729
|
+
- Tag-based: Uses git fetch --tags --force + git checkout
|
|
2730
|
+
"""
|
|
2731
|
+
library_name = request.library_name
|
|
2732
|
+
|
|
2733
|
+
# Validate library and prepare for git operation
|
|
2734
|
+
validation_result = await self._validate_and_prepare_library_for_git_operation(
|
|
2735
|
+
library_name=library_name,
|
|
2736
|
+
failure_result_class=UpdateLibraryResultFailure,
|
|
2737
|
+
operation_description="update",
|
|
2738
|
+
)
|
|
2739
|
+
if isinstance(validation_result, ResultPayloadFailure):
|
|
2740
|
+
return validation_result
|
|
2741
|
+
|
|
2742
|
+
old_version = validation_result.old_version
|
|
2743
|
+
library_file_path = validation_result.library_file_path
|
|
2744
|
+
library_dir = validation_result.library_dir
|
|
2745
|
+
|
|
2746
|
+
# Check if library is in a monorepo (multiple libraries in same git repository)
|
|
2747
|
+
if await asyncio.to_thread(is_monorepo, library_dir):
|
|
2748
|
+
details = f"Cannot update Library '{library_name}'. Repository contains multiple libraries and must be updated manually."
|
|
2749
|
+
return UpdateLibraryResultFailure(result_details=details)
|
|
2750
|
+
|
|
2751
|
+
# Perform git update (auto-detects branch vs tag workflow)
|
|
2752
|
+
try:
|
|
2753
|
+
await asyncio.to_thread(
|
|
2754
|
+
update_library_git,
|
|
2755
|
+
library_dir,
|
|
2756
|
+
overwrite_existing=request.overwrite_existing,
|
|
2757
|
+
)
|
|
2758
|
+
except (GitPullError, GitRepositoryError) as e:
|
|
2759
|
+
error_msg = str(e).lower()
|
|
2760
|
+
|
|
2761
|
+
# Check if error is retryable (uncommitted changes)
|
|
2762
|
+
retryable = "uncommitted changes" in error_msg or "unstaged changes" in error_msg
|
|
2763
|
+
|
|
2764
|
+
details = f"Failed to update Library '{library_name}': {e}"
|
|
2765
|
+
return UpdateLibraryResultFailure(result_details=details, retryable=retryable)
|
|
2766
|
+
|
|
2767
|
+
# Reload library
|
|
2768
|
+
reload_result = await self._reload_library_after_git_operation(
|
|
2769
|
+
library_name=library_name,
|
|
2770
|
+
library_file_path=library_file_path,
|
|
2771
|
+
failure_result_class=UpdateLibraryResultFailure,
|
|
2772
|
+
)
|
|
2773
|
+
if isinstance(reload_result, ResultPayloadFailure):
|
|
2774
|
+
return reload_result
|
|
2775
|
+
|
|
2776
|
+
new_version = reload_result
|
|
2777
|
+
|
|
2778
|
+
details = f"Successfully updated Library '{library_name}' from version {old_version} to {new_version}."
|
|
2779
|
+
return UpdateLibraryResultSuccess(
|
|
2780
|
+
old_version=old_version,
|
|
2781
|
+
new_version=new_version,
|
|
2782
|
+
result_details=details,
|
|
2783
|
+
)
|
|
2784
|
+
|
|
2785
|
+
async def switch_library_ref_request(self, request: SwitchLibraryRefRequest) -> ResultPayload:
|
|
2786
|
+
"""Switch a library to a different git branch or tag."""
|
|
2787
|
+
library_name = request.library_name
|
|
2788
|
+
ref_name = request.ref_name
|
|
2789
|
+
|
|
2790
|
+
# Validate library and prepare for git operation
|
|
2791
|
+
validation_result = await self._validate_and_prepare_library_for_git_operation(
|
|
2792
|
+
library_name=library_name,
|
|
2793
|
+
failure_result_class=SwitchLibraryRefResultFailure,
|
|
2794
|
+
operation_description="switch branch/tag for",
|
|
2795
|
+
)
|
|
2796
|
+
if isinstance(validation_result, ResultPayloadFailure):
|
|
2797
|
+
return validation_result
|
|
2798
|
+
|
|
2799
|
+
old_version = validation_result.old_version
|
|
2800
|
+
library_file_path = validation_result.library_file_path
|
|
2801
|
+
library_dir = validation_result.library_dir
|
|
2802
|
+
|
|
2803
|
+
# Get current ref (branch or tag) before switch
|
|
2804
|
+
try:
|
|
2805
|
+
old_ref = await asyncio.to_thread(get_current_ref, library_dir)
|
|
2806
|
+
if old_ref is None:
|
|
2807
|
+
details = f"Library '{library_name}' is not on a branch/tag or is not a git repository."
|
|
2808
|
+
return SwitchLibraryRefResultFailure(result_details=details)
|
|
2809
|
+
except GitRefError as e:
|
|
2810
|
+
details = f"Failed to get current branch/tag for Library '{library_name}': {e}"
|
|
2811
|
+
return SwitchLibraryRefResultFailure(result_details=details)
|
|
2812
|
+
|
|
2813
|
+
# Perform git ref switch (branch or tag)
|
|
2814
|
+
try:
|
|
2815
|
+
await asyncio.to_thread(switch_branch_or_tag, library_dir, ref_name)
|
|
2816
|
+
except (GitRefError, GitRepositoryError) as e:
|
|
2817
|
+
details = f"Failed to switch to '{ref_name}' for Library '{library_name}': {e}"
|
|
2818
|
+
return SwitchLibraryRefResultFailure(result_details=details)
|
|
2819
|
+
|
|
2820
|
+
# Reload library
|
|
2821
|
+
reload_result = await self._reload_library_after_git_operation(
|
|
2822
|
+
library_name=library_name,
|
|
2823
|
+
library_file_path=library_file_path,
|
|
2824
|
+
failure_result_class=SwitchLibraryRefResultFailure,
|
|
2825
|
+
)
|
|
2826
|
+
if isinstance(reload_result, ResultPayloadFailure):
|
|
2827
|
+
return reload_result
|
|
2828
|
+
|
|
2829
|
+
new_version = reload_result
|
|
2830
|
+
|
|
2831
|
+
# Get new ref (branch or tag) after switch
|
|
2832
|
+
try:
|
|
2833
|
+
new_ref = await asyncio.to_thread(get_current_ref, library_dir)
|
|
2834
|
+
if new_ref is None:
|
|
2835
|
+
new_ref = "unknown"
|
|
2836
|
+
except GitRefError:
|
|
2837
|
+
new_ref = "unknown"
|
|
2838
|
+
|
|
2839
|
+
details = f"Successfully switched Library '{library_name}' from '{old_ref}' (version {old_version}) to '{new_ref}' (version {new_version})."
|
|
2840
|
+
return SwitchLibraryRefResultSuccess(
|
|
2841
|
+
old_ref=old_ref,
|
|
2842
|
+
new_ref=new_ref,
|
|
2843
|
+
old_version=old_version,
|
|
2844
|
+
new_version=new_version,
|
|
2845
|
+
result_details=details,
|
|
2846
|
+
)
|
|
2847
|
+
|
|
2848
|
+
async def download_library_request(self, request: DownloadLibraryRequest) -> ResultPayload: # noqa: PLR0911, PLR0912, PLR0915, C901
|
|
2849
|
+
"""Download a library from a git repository."""
|
|
2850
|
+
git_url = request.git_url
|
|
2851
|
+
branch_tag_commit = request.branch_tag_commit
|
|
2852
|
+
target_directory_name = request.target_directory_name
|
|
2853
|
+
download_directory = request.download_directory
|
|
2854
|
+
|
|
2855
|
+
# Determine the parent directory for the download
|
|
2856
|
+
config_mgr = GriptapeNodes.ConfigManager()
|
|
2857
|
+
|
|
2858
|
+
if download_directory is not None:
|
|
2859
|
+
# Use custom download directory if provided
|
|
2860
|
+
libraries_path = Path(download_directory)
|
|
2861
|
+
else:
|
|
2862
|
+
# Use default from config
|
|
2863
|
+
libraries_dir_setting = config_mgr.get_config_value("libraries_directory")
|
|
2864
|
+
if not libraries_dir_setting:
|
|
2865
|
+
details = "Cannot download library: libraries_directory setting is not configured."
|
|
2866
|
+
return DownloadLibraryResultFailure(result_details=details)
|
|
2867
|
+
libraries_path = config_mgr.workspace_path / libraries_dir_setting
|
|
2868
|
+
|
|
2869
|
+
# Ensure parent directory exists
|
|
2870
|
+
libraries_path.mkdir(parents=True, exist_ok=True)
|
|
2871
|
+
|
|
2872
|
+
# Determine target directory name
|
|
2873
|
+
if target_directory_name is None:
|
|
2874
|
+
# Extract from git URL (e.g., "https://github.com/user/repo.git" -> "repo")
|
|
2875
|
+
target_directory_name = git_url.rstrip("/").split("/")[-1]
|
|
2876
|
+
target_directory_name = target_directory_name.removesuffix(".git")
|
|
2877
|
+
|
|
2878
|
+
# Construct full target path
|
|
2879
|
+
target_path = libraries_path / target_directory_name
|
|
2880
|
+
|
|
2881
|
+
# Check if target directory already exists
|
|
2882
|
+
skip_clone = False
|
|
2883
|
+
if target_path.exists():
|
|
2884
|
+
if request.overwrite_existing:
|
|
2885
|
+
# Delete existing directory before cloning
|
|
2886
|
+
delete_request = DeleteFileRequest(path=str(target_path), workspace_only=False)
|
|
2887
|
+
delete_result = await GriptapeNodes.ahandle_request(delete_request)
|
|
2888
|
+
|
|
2889
|
+
if isinstance(delete_result, DeleteFileResultFailure):
|
|
2890
|
+
details = f"Cannot delete existing directory at {target_path}: {delete_result.result_details}"
|
|
2891
|
+
return DownloadLibraryResultFailure(result_details=details)
|
|
2892
|
+
|
|
2893
|
+
logger.info("Deleted existing directory at %s for overwrite", target_path)
|
|
2894
|
+
else:
|
|
2895
|
+
# Check fail_on_exists flag
|
|
2896
|
+
if request.fail_on_exists:
|
|
2897
|
+
# Fail with retryable error for interactive CLI
|
|
2898
|
+
details = f"Cannot download library: target directory already exists at {target_path}"
|
|
2899
|
+
return DownloadLibraryResultFailure(result_details=details, retryable=True)
|
|
2900
|
+
|
|
2901
|
+
# Skip cloning since directory already exists, but continue with registration
|
|
2902
|
+
skip_clone = True
|
|
2903
|
+
logger.debug(
|
|
2904
|
+
"Library directory already exists at %s, skipping download but will proceed with registration",
|
|
2905
|
+
target_path,
|
|
2906
|
+
)
|
|
2907
|
+
|
|
2908
|
+
# Clone the repository (unless skipping because it already exists)
|
|
2909
|
+
if skip_clone:
|
|
2910
|
+
logger.debug("Using existing library directory at %s", target_path)
|
|
2911
|
+
else:
|
|
2912
|
+
try:
|
|
2913
|
+
await asyncio.to_thread(clone_repository, git_url, target_path, branch_tag_commit)
|
|
2914
|
+
except GitCloneError as e:
|
|
2915
|
+
details = f"Failed to clone repository from {git_url} to {target_path}: {e}"
|
|
2916
|
+
return DownloadLibraryResultFailure(result_details=details)
|
|
2917
|
+
|
|
2918
|
+
# Recursively search for griptape_nodes_library.json file
|
|
2919
|
+
library_json_path = find_file_in_directory(target_path, "griptape[-_]nodes[-_]library.json")
|
|
2920
|
+
if library_json_path is None:
|
|
2921
|
+
details = f"Downloaded library from {git_url} but no library JSON file found in {target_path}"
|
|
2922
|
+
return DownloadLibraryResultFailure(result_details=details)
|
|
2923
|
+
|
|
2924
|
+
try:
|
|
2925
|
+
with library_json_path.open() as f:
|
|
2926
|
+
library_data = json.load(f)
|
|
2927
|
+
except json.JSONDecodeError as e:
|
|
2928
|
+
details = f"Failed to parse griptape_nodes_library.json from downloaded library: {e}"
|
|
2929
|
+
return DownloadLibraryResultFailure(result_details=details)
|
|
2930
|
+
|
|
2931
|
+
# Extract library name
|
|
2932
|
+
library_name = library_data.get("name")
|
|
2933
|
+
if library_name is None:
|
|
2934
|
+
details = "Downloaded library has no 'name' field in griptape_nodes_library.json"
|
|
2935
|
+
return DownloadLibraryResultFailure(result_details=details)
|
|
2936
|
+
|
|
2937
|
+
# Automatically register the downloaded library (unless disabled for startup downloads)
|
|
2938
|
+
if request.auto_register:
|
|
2939
|
+
register_request = RegisterLibraryFromFileRequest(file_path=str(library_json_path))
|
|
2940
|
+
register_result = await GriptapeNodes.ahandle_request(register_request)
|
|
2941
|
+
if not register_result.succeeded():
|
|
2942
|
+
logger.warning(
|
|
2943
|
+
"Library '%s' was downloaded but registration failed: %s",
|
|
2944
|
+
library_name,
|
|
2945
|
+
register_result.result_details,
|
|
2946
|
+
)
|
|
2947
|
+
else:
|
|
2948
|
+
logger.info("Library '%s' registered successfully", library_name)
|
|
2949
|
+
|
|
2950
|
+
# Add library JSON file path to config so it's registered on future startups
|
|
2951
|
+
libraries_to_register = config_mgr.get_config_value(LIBRARIES_TO_REGISTER_KEY, default=[])
|
|
2952
|
+
library_json_str = str(library_json_path)
|
|
2953
|
+
if library_json_str not in libraries_to_register:
|
|
2954
|
+
libraries_to_register.append(library_json_str)
|
|
2955
|
+
config_mgr.set_config_value(LIBRARIES_TO_REGISTER_KEY, libraries_to_register)
|
|
2956
|
+
logger.info("Added library '%s' to config for auto-registration on startup", library_name)
|
|
2957
|
+
|
|
2958
|
+
if skip_clone:
|
|
2959
|
+
details = f"Library '{library_name}' already exists at {target_path} and has been registered"
|
|
2960
|
+
else:
|
|
2961
|
+
details = f"Successfully downloaded library '{library_name}' from {git_url} to {target_path}"
|
|
2962
|
+
return DownloadLibraryResultSuccess(
|
|
2963
|
+
library_name=library_name,
|
|
2964
|
+
library_path=str(library_json_path),
|
|
2965
|
+
result_details=details,
|
|
2966
|
+
)
|
|
2967
|
+
|
|
2968
|
+
async def install_library_dependencies_request(self, request: InstallLibraryDependenciesRequest) -> ResultPayload: # noqa: PLR0911
|
|
2969
|
+
"""Install dependencies for a library."""
|
|
2970
|
+
library_file_path = request.library_file_path
|
|
2971
|
+
|
|
2972
|
+
# Load library metadata from file
|
|
2973
|
+
metadata_request = LoadLibraryMetadataFromFileRequest(file_path=library_file_path)
|
|
2974
|
+
metadata_result = self.load_library_metadata_from_file_request(metadata_request)
|
|
2975
|
+
|
|
2976
|
+
if not isinstance(metadata_result, LoadLibraryMetadataFromFileResultSuccess):
|
|
2977
|
+
details = f"Failed to load library metadata from {library_file_path}: {metadata_result.result_details}"
|
|
2978
|
+
return InstallLibraryDependenciesResultFailure(result_details=details)
|
|
2979
|
+
|
|
2980
|
+
library_data = metadata_result.library_schema
|
|
2981
|
+
library_name = library_data.name
|
|
2982
|
+
library_metadata = library_data.metadata
|
|
2983
|
+
|
|
2984
|
+
if not library_metadata.dependencies or not library_metadata.dependencies.pip_dependencies:
|
|
2985
|
+
details = f"Library '{library_name}' has no dependencies to install"
|
|
2986
|
+
logger.info(details)
|
|
2987
|
+
return InstallLibraryDependenciesResultSuccess(
|
|
2988
|
+
library_name=library_name, dependencies_installed=0, result_details=details
|
|
2989
|
+
)
|
|
2990
|
+
|
|
2991
|
+
pip_dependencies = library_metadata.dependencies.pip_dependencies
|
|
2992
|
+
pip_install_flags = library_metadata.dependencies.pip_install_flags or []
|
|
2993
|
+
|
|
2994
|
+
# Get venv path and initialize it
|
|
2995
|
+
venv_path = self._get_library_venv_path(library_name, library_file_path)
|
|
2996
|
+
|
|
2997
|
+
try:
|
|
2998
|
+
library_venv_python_path = await self._init_library_venv(venv_path)
|
|
2999
|
+
except RuntimeError as e:
|
|
3000
|
+
details = f"Failed to initialize venv for library '{library_name}': {e}"
|
|
3001
|
+
return InstallLibraryDependenciesResultFailure(result_details=details)
|
|
3002
|
+
|
|
3003
|
+
if not self._can_write_to_venv_location(library_venv_python_path):
|
|
3004
|
+
details = f"Venv location for library '{library_name}' at {venv_path} is not writable"
|
|
3005
|
+
logger.warning(details)
|
|
3006
|
+
return InstallLibraryDependenciesResultFailure(result_details=details)
|
|
3007
|
+
|
|
3008
|
+
# Check disk space
|
|
3009
|
+
config_manager = GriptapeNodes.ConfigManager()
|
|
3010
|
+
min_space_gb = config_manager.get_config_value("minimum_disk_space_gb_libraries")
|
|
3011
|
+
if not OSManager.check_available_disk_space(Path(venv_path), min_space_gb):
|
|
3012
|
+
error_msg = OSManager.format_disk_space_error(Path(venv_path))
|
|
3013
|
+
details = f"Insufficient disk space for dependencies (requires {min_space_gb} GB) for library '{library_name}': {error_msg}"
|
|
3014
|
+
return InstallLibraryDependenciesResultFailure(result_details=details)
|
|
3015
|
+
|
|
3016
|
+
# Install dependencies
|
|
3017
|
+
logger.info("Installing %d dependencies for library '%s'", len(pip_dependencies), library_name)
|
|
3018
|
+
is_debug = config_manager.get_config_value("log_level").upper() == "DEBUG"
|
|
3019
|
+
|
|
3020
|
+
try:
|
|
3021
|
+
await subprocess_run(
|
|
3022
|
+
[
|
|
3023
|
+
sys.executable,
|
|
3024
|
+
"-m",
|
|
3025
|
+
"uv",
|
|
3026
|
+
"pip",
|
|
3027
|
+
"install",
|
|
3028
|
+
*pip_dependencies,
|
|
3029
|
+
*pip_install_flags,
|
|
3030
|
+
"--python",
|
|
3031
|
+
str(library_venv_python_path),
|
|
3032
|
+
],
|
|
3033
|
+
check=True,
|
|
3034
|
+
capture_output=not is_debug,
|
|
3035
|
+
text=True,
|
|
3036
|
+
)
|
|
3037
|
+
except subprocess.CalledProcessError as e:
|
|
3038
|
+
details = f"Failed to install dependencies for library '{library_name}': return code={e.returncode}, stderr={e.stderr}"
|
|
3039
|
+
return InstallLibraryDependenciesResultFailure(result_details=details)
|
|
3040
|
+
|
|
3041
|
+
details = f"Successfully installed {len(pip_dependencies)} dependencies for library '{library_name}'"
|
|
3042
|
+
logger.info(details)
|
|
3043
|
+
return InstallLibraryDependenciesResultSuccess(
|
|
3044
|
+
library_name=library_name, dependencies_installed=len(pip_dependencies), result_details=details
|
|
3045
|
+
)
|
|
3046
|
+
|
|
3047
|
+
async def sync_libraries_request(self, request: SyncLibrariesRequest) -> ResultPayload: # noqa: C901, PLR0915
|
|
3048
|
+
"""Sync all libraries to latest versions and ensure dependencies are installed."""
|
|
3049
|
+
# Phase 1: Download missing libraries from both config keys
|
|
3050
|
+
config_mgr = GriptapeNodes.ConfigManager()
|
|
3051
|
+
|
|
3052
|
+
# Collect git URLs from both config keys
|
|
3053
|
+
download_config = config_mgr.get_config_value(LIBRARIES_TO_DOWNLOAD_KEY, default=[])
|
|
3054
|
+
register_config = config_mgr.get_config_value(LIBRARIES_TO_REGISTER_KEY, default=[])
|
|
3055
|
+
git_urls_from_register = [entry for entry in register_config if is_git_url(entry)]
|
|
3056
|
+
|
|
3057
|
+
# Combine and deduplicate
|
|
3058
|
+
all_git_urls = list(set(download_config + git_urls_from_register))
|
|
3059
|
+
|
|
3060
|
+
# Use shared download method
|
|
3061
|
+
update_summary = {}
|
|
3062
|
+
libraries_downloaded = 0
|
|
3063
|
+
|
|
3064
|
+
if all_git_urls:
|
|
3065
|
+
logger.info("Found %d git URLs, downloading missing libraries", len(all_git_urls))
|
|
3066
|
+
download_results = await self._download_libraries_from_git_urls(all_git_urls)
|
|
3067
|
+
|
|
3068
|
+
# Process results for summary
|
|
3069
|
+
for git_url, result in download_results.items():
|
|
3070
|
+
if result["success"]:
|
|
3071
|
+
libraries_downloaded += 1
|
|
3072
|
+
update_summary[result["library_name"]] = {
|
|
3073
|
+
"status": "downloaded",
|
|
3074
|
+
"git_url": git_url,
|
|
3075
|
+
}
|
|
3076
|
+
elif result.get("error"):
|
|
3077
|
+
logger.warning("Download failed for '%s': %s", git_url, result["error"])
|
|
3078
|
+
|
|
3079
|
+
logger.info("Downloaded %d new libraries", libraries_downloaded)
|
|
3080
|
+
|
|
3081
|
+
# Phase 2: Load libraries to ensure newly downloaded ones are registered
|
|
3082
|
+
logger.info("Loading libraries to register newly downloaded ones")
|
|
3083
|
+
load_request = LoadLibrariesRequest()
|
|
3084
|
+
load_result = await GriptapeNodes.ahandle_request(load_request)
|
|
3085
|
+
|
|
3086
|
+
if not isinstance(load_result, LoadLibrariesResultSuccess):
|
|
3087
|
+
logger.warning("Failed to load libraries after download: %s", load_result.result_details)
|
|
3088
|
+
# Continue anyway - we can still update previously registered libraries
|
|
3089
|
+
|
|
3090
|
+
# Phase 3: Check and update all registered libraries
|
|
3091
|
+
# Get all registered libraries
|
|
3092
|
+
list_result = await GriptapeNodes.ahandle_request(ListRegisteredLibrariesRequest())
|
|
3093
|
+
if not isinstance(list_result, ListRegisteredLibrariesResultSuccess):
|
|
3094
|
+
details = "Failed to list registered libraries for sync"
|
|
3095
|
+
return SyncLibrariesResultFailure(result_details=details)
|
|
3096
|
+
|
|
3097
|
+
libraries_to_check = list_result.libraries
|
|
3098
|
+
|
|
3099
|
+
logger.info("Checking %d registered libraries for updates", len(libraries_to_check))
|
|
3100
|
+
|
|
3101
|
+
# Check all libraries for updates concurrently using task group
|
|
3102
|
+
async def check_library_for_update(library_name: str) -> tuple[str, ResultPayload]:
|
|
3103
|
+
"""Check a single library for updates."""
|
|
3104
|
+
logger.info("Checking library '%s' for updates", library_name)
|
|
3105
|
+
check_result = await GriptapeNodes.ahandle_request(
|
|
3106
|
+
CheckLibraryUpdateRequest(library_name=library_name, failure_log_level=logging.DEBUG)
|
|
3107
|
+
)
|
|
3108
|
+
return library_name, check_result
|
|
3109
|
+
|
|
3110
|
+
# Gather all check results concurrently
|
|
3111
|
+
check_results: dict[str, ResultPayload] = {}
|
|
3112
|
+
async with asyncio.TaskGroup() as tg:
|
|
3113
|
+
tasks = [tg.create_task(check_library_for_update(lib)) for lib in libraries_to_check]
|
|
3114
|
+
|
|
3115
|
+
# Collect results from completed tasks
|
|
3116
|
+
for task in tasks:
|
|
3117
|
+
library_name, result = task.result()
|
|
3118
|
+
check_results[library_name] = result
|
|
3119
|
+
|
|
3120
|
+
# Process check results and determine which libraries need updates
|
|
3121
|
+
libraries_checked = len(libraries_to_check)
|
|
3122
|
+
libraries_updated = 0
|
|
3123
|
+
libraries_to_update: list[LibraryUpdateInfo] = []
|
|
3124
|
+
|
|
3125
|
+
for library_name, check_result in check_results.items():
|
|
3126
|
+
if not isinstance(check_result, CheckLibraryUpdateResultSuccess):
|
|
3127
|
+
logger.warning(
|
|
3128
|
+
"Failed to check for updates for library '%s', skipping: %s",
|
|
3129
|
+
library_name,
|
|
3130
|
+
str(check_result.result_details),
|
|
3131
|
+
)
|
|
3132
|
+
continue
|
|
3133
|
+
|
|
3134
|
+
if not check_result.has_update:
|
|
3135
|
+
logger.info("Library '%s' is up to date (version %s)", library_name, check_result.current_version)
|
|
3136
|
+
continue
|
|
3137
|
+
|
|
3138
|
+
# Library has an update available
|
|
3139
|
+
old_version = check_result.current_version or "unknown"
|
|
3140
|
+
new_version = check_result.latest_version or "unknown"
|
|
3141
|
+
logger.info("Library '%s' has update available: %s -> %s", library_name, old_version, new_version)
|
|
3142
|
+
libraries_to_update.append(
|
|
3143
|
+
LibraryUpdateInfo(library_name=library_name, old_version=old_version, new_version=new_version)
|
|
3144
|
+
)
|
|
3145
|
+
|
|
3146
|
+
# Update libraries concurrently using task group
|
|
3147
|
+
async def update_library(library_name: str, old_version: str, new_version: str) -> LibraryUpdateResult:
|
|
3148
|
+
"""Update a single library."""
|
|
3149
|
+
logger.info("Updating library '%s' from %s to %s", library_name, old_version, new_version)
|
|
3150
|
+
update_result = await GriptapeNodes.ahandle_request(
|
|
3151
|
+
UpdateLibraryRequest(
|
|
3152
|
+
library_name=library_name,
|
|
3153
|
+
overwrite_existing=request.overwrite_existing,
|
|
3154
|
+
)
|
|
3155
|
+
)
|
|
3156
|
+
return LibraryUpdateResult(
|
|
3157
|
+
library_name=library_name,
|
|
3158
|
+
old_version=old_version,
|
|
3159
|
+
new_version=new_version,
|
|
3160
|
+
result=update_result,
|
|
3161
|
+
)
|
|
3162
|
+
|
|
3163
|
+
# Gather all update results concurrently
|
|
3164
|
+
async with asyncio.TaskGroup() as tg:
|
|
3165
|
+
update_tasks = [
|
|
3166
|
+
tg.create_task(update_library(info.library_name, info.old_version, info.new_version))
|
|
3167
|
+
for info in libraries_to_update
|
|
3168
|
+
]
|
|
3169
|
+
|
|
3170
|
+
# Collect update results
|
|
3171
|
+
for task in update_tasks:
|
|
3172
|
+
result = task.result()
|
|
3173
|
+
library_name = result.library_name
|
|
3174
|
+
old_version = result.old_version
|
|
3175
|
+
new_version = result.new_version
|
|
3176
|
+
update_result = result.result
|
|
3177
|
+
|
|
3178
|
+
if not isinstance(update_result, UpdateLibraryResultSuccess):
|
|
3179
|
+
logger.error("Failed to update library '%s': %s", library_name, update_result.result_details)
|
|
3180
|
+
update_summary[library_name] = {
|
|
3181
|
+
"old_version": old_version,
|
|
3182
|
+
"new_version": old_version,
|
|
3183
|
+
"status": "failed",
|
|
3184
|
+
"error": update_result.result_details,
|
|
3185
|
+
}
|
|
3186
|
+
continue
|
|
3187
|
+
|
|
3188
|
+
libraries_updated += 1
|
|
3189
|
+
update_summary[library_name] = {
|
|
3190
|
+
"old_version": update_result.old_version,
|
|
3191
|
+
"new_version": update_result.new_version,
|
|
3192
|
+
"status": "updated",
|
|
3193
|
+
}
|
|
3194
|
+
logger.info(
|
|
3195
|
+
"Successfully updated library '%s' from %s to %s",
|
|
3196
|
+
library_name,
|
|
3197
|
+
update_result.old_version,
|
|
3198
|
+
update_result.new_version,
|
|
3199
|
+
)
|
|
3200
|
+
|
|
3201
|
+
# Build result details
|
|
3202
|
+
details = f"Downloaded {libraries_downloaded} libraries. Checked {libraries_checked} libraries. {libraries_updated} updated."
|
|
3203
|
+
logger.info(details)
|
|
3204
|
+
return SyncLibrariesResultSuccess(
|
|
3205
|
+
libraries_downloaded=libraries_downloaded,
|
|
3206
|
+
libraries_checked=libraries_checked,
|
|
3207
|
+
libraries_updated=libraries_updated,
|
|
3208
|
+
update_summary=update_summary,
|
|
3209
|
+
result_details=details,
|
|
3210
|
+
)
|
|
3211
|
+
|
|
3212
|
+
async def inspect_library_repo_request(self, request: InspectLibraryRepoRequest) -> ResultPayload:
|
|
3213
|
+
"""Inspect a library's metadata from a git repository without downloading the full repository."""
|
|
3214
|
+
git_url = request.git_url
|
|
3215
|
+
ref = request.ref
|
|
3216
|
+
|
|
3217
|
+
# Normalize GitHub shorthand to full URL
|
|
3218
|
+
from griptape_nodes.utils.git_utils import normalize_github_url, sparse_checkout_library_json
|
|
3219
|
+
|
|
3220
|
+
normalized_url = normalize_github_url(git_url)
|
|
3221
|
+
logger.info("Inspecting library metadata from '%s' (ref: %s)", normalized_url, ref)
|
|
3222
|
+
|
|
3223
|
+
# Perform sparse checkout to get library JSON
|
|
3224
|
+
try:
|
|
3225
|
+
library_version, commit_sha, library_data_raw = sparse_checkout_library_json(normalized_url, ref)
|
|
3226
|
+
except GitCloneError as e:
|
|
3227
|
+
details = f"Failed to inspect library from {normalized_url}: {e}"
|
|
3228
|
+
logger.error(details)
|
|
3229
|
+
return InspectLibraryRepoResultFailure(result_details=details)
|
|
3230
|
+
|
|
3231
|
+
# Validate and create LibrarySchema
|
|
3232
|
+
try:
|
|
3233
|
+
library_schema = LibrarySchema(**library_data_raw)
|
|
3234
|
+
except Exception as e:
|
|
3235
|
+
details = f"Invalid library schema from {normalized_url}: {e}"
|
|
3236
|
+
logger.error(details)
|
|
3237
|
+
return InspectLibraryRepoResultFailure(result_details=details)
|
|
3238
|
+
|
|
3239
|
+
# Return success with full library metadata
|
|
3240
|
+
details = f"Successfully inspected library '{library_schema.name}' (version {library_version}) from {normalized_url} at commit {commit_sha[:7]}"
|
|
3241
|
+
logger.info(details)
|
|
3242
|
+
return InspectLibraryRepoResultSuccess(
|
|
3243
|
+
library_schema=library_schema,
|
|
3244
|
+
commit_sha=commit_sha,
|
|
3245
|
+
git_url=normalized_url,
|
|
3246
|
+
ref=ref,
|
|
3247
|
+
result_details=details,
|
|
3248
|
+
)
|