griptape-nodes 0.65.6__py3-none-any.whl → 0.66.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- griptape_nodes/common/node_executor.py +352 -27
- griptape_nodes/drivers/storage/base_storage_driver.py +12 -3
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +18 -2
- griptape_nodes/drivers/storage/local_storage_driver.py +42 -5
- griptape_nodes/exe_types/base_iterative_nodes.py +0 -1
- griptape_nodes/exe_types/connections.py +42 -0
- griptape_nodes/exe_types/core_types.py +2 -2
- griptape_nodes/exe_types/node_groups/__init__.py +2 -1
- griptape_nodes/exe_types/node_groups/base_iterative_node_group.py +177 -0
- griptape_nodes/exe_types/node_groups/base_node_group.py +1 -0
- griptape_nodes/exe_types/node_groups/subflow_node_group.py +35 -2
- griptape_nodes/exe_types/param_types/parameter_audio.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_bool.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_button.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_float.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_image.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_int.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_number.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_string.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_three_d.py +1 -1
- griptape_nodes/exe_types/param_types/parameter_video.py +1 -1
- griptape_nodes/machines/control_flow.py +5 -4
- griptape_nodes/machines/dag_builder.py +121 -55
- griptape_nodes/machines/fsm.py +10 -0
- griptape_nodes/machines/parallel_resolution.py +39 -38
- griptape_nodes/machines/sequential_resolution.py +29 -3
- griptape_nodes/node_library/library_registry.py +41 -2
- griptape_nodes/retained_mode/events/library_events.py +147 -8
- griptape_nodes/retained_mode/events/os_events.py +12 -4
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +2 -0
- griptape_nodes/retained_mode/managers/fitness_problems/libraries/incompatible_requirements_problem.py +34 -0
- griptape_nodes/retained_mode/managers/flow_manager.py +133 -20
- griptape_nodes/retained_mode/managers/library_manager.py +1324 -564
- griptape_nodes/retained_mode/managers/node_manager.py +9 -3
- griptape_nodes/retained_mode/managers/os_manager.py +429 -65
- griptape_nodes/retained_mode/managers/resource_types/compute_resource.py +82 -0
- griptape_nodes/retained_mode/managers/resource_types/os_resource.py +17 -0
- griptape_nodes/retained_mode/managers/static_files_manager.py +21 -8
- griptape_nodes/retained_mode/managers/version_compatibility_manager.py +3 -3
- griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +5 -5
- griptape_nodes/version_compatibility/versions/v0_65_4/__init__.py +5 -0
- griptape_nodes/version_compatibility/versions/v0_65_4/run_in_parallel_to_run_in_order.py +79 -0
- griptape_nodes/version_compatibility/versions/v0_65_5/__init__.py +5 -0
- griptape_nodes/version_compatibility/versions/v0_65_5/flux_2_removed_parameters.py +85 -0
- {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.1.dist-info}/METADATA +1 -1
- {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.1.dist-info}/RECORD +48 -53
- griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +0 -45
- griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +0 -191
- griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +0 -346
- griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +0 -439
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +0 -17
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +0 -82
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +0 -116
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +0 -367
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +0 -104
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +0 -155
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +0 -18
- griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +0 -12
- {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.1.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.65.6.dist-info → griptape_nodes-0.66.1.dist-info}/entry_points.txt +0 -0
|
@@ -11,6 +11,7 @@ import sys
|
|
|
11
11
|
import sysconfig
|
|
12
12
|
from collections import defaultdict
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
|
+
from enum import StrEnum
|
|
14
15
|
from importlib.resources import files
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import TYPE_CHECKING, Any, Generic, NamedTuple, TypeVar, cast
|
|
@@ -57,9 +58,16 @@ from griptape_nodes.retained_mode.events.library_events import (
|
|
|
57
58
|
CheckLibraryUpdateRequest,
|
|
58
59
|
CheckLibraryUpdateResultFailure,
|
|
59
60
|
CheckLibraryUpdateResultSuccess,
|
|
61
|
+
DiscoveredLibrary,
|
|
62
|
+
DiscoverLibrariesRequest,
|
|
63
|
+
DiscoverLibrariesResultFailure,
|
|
64
|
+
DiscoverLibrariesResultSuccess,
|
|
60
65
|
DownloadLibraryRequest,
|
|
61
66
|
DownloadLibraryResultFailure,
|
|
62
67
|
DownloadLibraryResultSuccess,
|
|
68
|
+
EvaluateLibraryFitnessRequest,
|
|
69
|
+
EvaluateLibraryFitnessResultFailure,
|
|
70
|
+
EvaluateLibraryFitnessResultSuccess,
|
|
63
71
|
GetAllInfoForAllLibrariesRequest,
|
|
64
72
|
GetAllInfoForAllLibrariesResultFailure,
|
|
65
73
|
GetAllInfoForAllLibrariesResultSuccess,
|
|
@@ -106,6 +114,9 @@ from griptape_nodes.retained_mode.events.library_events import (
|
|
|
106
114
|
ReloadAllLibrariesRequest,
|
|
107
115
|
ReloadAllLibrariesResultFailure,
|
|
108
116
|
ReloadAllLibrariesResultSuccess,
|
|
117
|
+
ScanSandboxDirectoryRequest,
|
|
118
|
+
ScanSandboxDirectoryResultFailure,
|
|
119
|
+
ScanSandboxDirectoryResultSuccess,
|
|
109
120
|
SwitchLibraryRefRequest,
|
|
110
121
|
SwitchLibraryRefResultFailure,
|
|
111
122
|
SwitchLibraryRefResultSuccess,
|
|
@@ -123,8 +134,15 @@ from griptape_nodes.retained_mode.events.object_events import ClearAllObjectStat
|
|
|
123
134
|
from griptape_nodes.retained_mode.events.os_events import (
|
|
124
135
|
DeleteFileRequest,
|
|
125
136
|
DeleteFileResultFailure,
|
|
137
|
+
WriteFileRequest,
|
|
126
138
|
)
|
|
127
139
|
from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
|
|
140
|
+
from griptape_nodes.retained_mode.events.resource_events import (
|
|
141
|
+
GetResourceInstanceStatusRequest,
|
|
142
|
+
GetResourceInstanceStatusResultSuccess,
|
|
143
|
+
ListCompatibleResourceInstancesRequest,
|
|
144
|
+
ListCompatibleResourceInstancesResultSuccess,
|
|
145
|
+
)
|
|
128
146
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
129
147
|
from griptape_nodes.retained_mode.managers.fitness_problems.libraries import (
|
|
130
148
|
AdvancedLibraryLoadFailureProblem,
|
|
@@ -133,6 +151,7 @@ from griptape_nodes.retained_mode.managers.fitness_problems.libraries import (
|
|
|
133
151
|
CreateConfigCategoryProblem,
|
|
134
152
|
DuplicateLibraryProblem,
|
|
135
153
|
EngineVersionErrorProblem,
|
|
154
|
+
IncompatibleRequirementsProblem,
|
|
136
155
|
InvalidVersionStringProblem,
|
|
137
156
|
LibraryJsonDecodeProblem,
|
|
138
157
|
LibraryLoadExceptionProblem,
|
|
@@ -147,11 +166,6 @@ from griptape_nodes.retained_mode.managers.fitness_problems.libraries import (
|
|
|
147
166
|
SandboxDirectoryMissingProblem,
|
|
148
167
|
UpdateConfigCategoryProblem,
|
|
149
168
|
)
|
|
150
|
-
from griptape_nodes.retained_mode.managers.library_lifecycle.library_directory import LibraryDirectory
|
|
151
|
-
from griptape_nodes.retained_mode.managers.library_lifecycle.library_provenance.local_file import (
|
|
152
|
-
LibraryProvenanceLocalFile,
|
|
153
|
-
)
|
|
154
|
-
from griptape_nodes.retained_mode.managers.library_lifecycle.library_status import LibraryStatus
|
|
155
169
|
from griptape_nodes.retained_mode.managers.os_manager import OSManager
|
|
156
170
|
from griptape_nodes.retained_mode.managers.settings import LIBRARIES_TO_DOWNLOAD_KEY, LIBRARIES_TO_REGISTER_KEY
|
|
157
171
|
from griptape_nodes.utils.async_utils import subprocess_run
|
|
@@ -230,20 +244,30 @@ class LibraryManager:
|
|
|
230
244
|
LIBRARY_CONFIG_FILENAME = "griptape_nodes_library.json"
|
|
231
245
|
LIBRARY_CONFIG_GLOB_PATTERN = "griptape[_-]nodes[_-]library.json"
|
|
232
246
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
247
|
+
# Sandbox library constants
|
|
248
|
+
UNRESOLVED_SANDBOX_CLASS_NAME = "<NOT YET RESOLVED>"
|
|
249
|
+
SANDBOX_CATEGORY_NAME = "Griptape Nodes Sandbox"
|
|
236
250
|
|
|
237
|
-
|
|
238
|
-
"""
|
|
251
|
+
_library_file_path_to_info: dict[str, LibraryInfo]
|
|
239
252
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
library_name: str | None = None
|
|
243
|
-
library_version: str | None = None
|
|
244
|
-
problems: list[LibraryProblem] = field(default_factory=list)
|
|
253
|
+
class LibraryLifecycleState(StrEnum):
|
|
254
|
+
"""Lifecycle states for library loading."""
|
|
245
255
|
|
|
246
|
-
|
|
256
|
+
FAILURE = "failure"
|
|
257
|
+
DISCOVERED = "discovered"
|
|
258
|
+
METADATA_LOADED = "metadata_loaded"
|
|
259
|
+
EVALUATED = "evaluated"
|
|
260
|
+
DEPENDENCIES_INSTALLED = "dependencies_installed"
|
|
261
|
+
LOADED = "loaded"
|
|
262
|
+
|
|
263
|
+
class LibraryFitness(StrEnum):
|
|
264
|
+
"""Fitness of the library that was attempted to be loaded."""
|
|
265
|
+
|
|
266
|
+
GOOD = "GOOD" # No errors detected during loading. Registered.
|
|
267
|
+
FLAWED = "FLAWED" # Some errors detected, but recoverable. Registered.
|
|
268
|
+
UNUSABLE = "UNUSABLE" # Errors detected and not recoverable. Not registered.
|
|
269
|
+
MISSING = "MISSING" # File not found. Not registered.
|
|
270
|
+
NOT_EVALUATED = "NOT_EVALUATED" # Library has not been evaluated yet.
|
|
247
271
|
|
|
248
272
|
@dataclass
|
|
249
273
|
class RegisteredEventHandler(Generic[TRegisteredEventData]):
|
|
@@ -257,6 +281,39 @@ class LibraryManager:
|
|
|
257
281
|
library_data: LibrarySchema
|
|
258
282
|
event_data: TRegisteredEventData | None = None
|
|
259
283
|
|
|
284
|
+
@dataclass
|
|
285
|
+
class LibraryInfo:
|
|
286
|
+
"""Information about a library that was attempted to be loaded.
|
|
287
|
+
|
|
288
|
+
Tracks the lifecycle state (where we are in the loading process) and fitness (health/quality).
|
|
289
|
+
Includes the file path and any problems encountered during loading.
|
|
290
|
+
|
|
291
|
+
Attributes:
|
|
292
|
+
lifecycle_state: Current phase of the library loading lifecycle (DISCOVERED → METADATA_LOADED →
|
|
293
|
+
EVALUATED → DEPENDENCIES_INSTALLED → LOADED or FAILURE at any phase)
|
|
294
|
+
fitness: Health/quality assessment of the library (GOOD, FLAWED, UNUSABLE, MISSING, NOT_EVALUATED)
|
|
295
|
+
library_path: Absolute path to the library JSON file or sandbox directory
|
|
296
|
+
is_sandbox: True if this is a sandbox library (user-created nodes in workspace), False for regular libraries
|
|
297
|
+
library_name: Name of the library from metadata (None until METADATA_LOADED phase)
|
|
298
|
+
library_version: Schema version from metadata (None until METADATA_LOADED phase)
|
|
299
|
+
problems: List of issues encountered during any phase (version incompatibilities, node load failures, etc.)
|
|
300
|
+
Problems accumulate across lifecycle phases and determine final fitness level.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
lifecycle_state: LibraryManager.LibraryLifecycleState
|
|
304
|
+
fitness: LibraryManager.LibraryFitness
|
|
305
|
+
library_path: str
|
|
306
|
+
is_sandbox: bool
|
|
307
|
+
library_name: str | None = None
|
|
308
|
+
library_version: str | None = None
|
|
309
|
+
problems: list[LibraryProblem] = field(default_factory=list)
|
|
310
|
+
|
|
311
|
+
class RegisterLibraryPrerequisites(NamedTuple):
|
|
312
|
+
"""Prerequisites established for library loading."""
|
|
313
|
+
|
|
314
|
+
library_info: LibraryManager.LibraryInfo
|
|
315
|
+
file_path: str
|
|
316
|
+
|
|
260
317
|
# Stable module namespace mappings for workflow serialization
|
|
261
318
|
# These mappings ensure that dynamically loaded modules can be reliably imported
|
|
262
319
|
# in generated workflow code by providing stable, predictable import paths.
|
|
@@ -284,8 +341,6 @@ class LibraryManager:
|
|
|
284
341
|
self._library_event_handler_mappings: dict[
|
|
285
342
|
type[Payload], dict[str, LibraryManager.RegisteredEventHandler[Any]]
|
|
286
343
|
] = {}
|
|
287
|
-
# LibraryDirectory owns the FSMs and manages library lifecycle
|
|
288
|
-
self._library_directory = LibraryDirectory()
|
|
289
344
|
self._libraries_loading_complete = asyncio.Event()
|
|
290
345
|
|
|
291
346
|
event_manager.assign_manager_to_request_type(
|
|
@@ -327,6 +382,7 @@ class LibraryManager:
|
|
|
327
382
|
event_manager.assign_manager_to_request_type(
|
|
328
383
|
LoadMetadataForAllLibrariesRequest, self.load_metadata_for_all_libraries_request
|
|
329
384
|
)
|
|
385
|
+
event_manager.assign_manager_to_request_type(ScanSandboxDirectoryRequest, self.scan_sandbox_directory_request)
|
|
330
386
|
event_manager.assign_manager_to_request_type(
|
|
331
387
|
UnloadLibraryFromRegistryRequest, self.unload_library_from_registry_request
|
|
332
388
|
)
|
|
@@ -371,25 +427,25 @@ class LibraryManager:
|
|
|
371
427
|
|
|
372
428
|
# Status emojis mapping
|
|
373
429
|
status_emoji = {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
430
|
+
LibraryManager.LibraryFitness.GOOD: "[green]OK[/green]",
|
|
431
|
+
LibraryManager.LibraryFitness.FLAWED: "[yellow]![/yellow]",
|
|
432
|
+
LibraryManager.LibraryFitness.UNUSABLE: "[red]X[/red]",
|
|
433
|
+
LibraryManager.LibraryFitness.MISSING: "[red]?[/red]",
|
|
378
434
|
}
|
|
379
435
|
|
|
380
436
|
# Status text mapping (colored)
|
|
381
437
|
status_text = {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
438
|
+
LibraryManager.LibraryFitness.GOOD: "[green](GOOD)[/green]",
|
|
439
|
+
LibraryManager.LibraryFitness.FLAWED: "[yellow](FLAWED)[/yellow]",
|
|
440
|
+
LibraryManager.LibraryFitness.UNUSABLE: "[red](UNUSABLE)[/red]",
|
|
441
|
+
LibraryManager.LibraryFitness.MISSING: "[red](MISSING)[/red]",
|
|
386
442
|
}
|
|
387
443
|
|
|
388
444
|
# Add rows for each library info
|
|
389
445
|
for lib_info in library_infos:
|
|
390
446
|
# Library column with emoji, name, version, colored status, and file path underneath
|
|
391
|
-
emoji = status_emoji.get(lib_info.
|
|
392
|
-
colored_status = status_text.get(lib_info.
|
|
447
|
+
emoji = status_emoji.get(lib_info.fitness, "ERROR: Unknown/Unexpected Library Status")
|
|
448
|
+
colored_status = status_text.get(lib_info.fitness, "(UNKNOWN)")
|
|
393
449
|
name = lib_info.library_name if lib_info.library_name else "*UNKNOWN*"
|
|
394
450
|
|
|
395
451
|
library_version = lib_info.library_version
|
|
@@ -536,7 +592,9 @@ class LibraryManager:
|
|
|
536
592
|
result = GetLibraryMetadataResultSuccess(metadata=metadata, result_details=details)
|
|
537
593
|
return result
|
|
538
594
|
|
|
539
|
-
def load_library_metadata_from_file_request(
|
|
595
|
+
def load_library_metadata_from_file_request( # noqa: PLR0911
|
|
596
|
+
self, request: LoadLibraryMetadataFromFileRequest
|
|
597
|
+
) -> LoadLibraryMetadataFromFileResultSuccess | LoadLibraryMetadataFromFileResultFailure:
|
|
540
598
|
"""Load library metadata from a JSON file without loading the actual node modules.
|
|
541
599
|
|
|
542
600
|
This method provides a lightweight way to get library schema information
|
|
@@ -553,7 +611,7 @@ class LibraryManager:
|
|
|
553
611
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
554
612
|
library_path=file_path,
|
|
555
613
|
library_name=None,
|
|
556
|
-
status=
|
|
614
|
+
status=LibraryManager.LibraryFitness.MISSING,
|
|
557
615
|
problems=[LibraryNotFoundProblem(library_path=str(json_path))],
|
|
558
616
|
result_details=details,
|
|
559
617
|
)
|
|
@@ -567,7 +625,7 @@ class LibraryManager:
|
|
|
567
625
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
568
626
|
library_path=file_path,
|
|
569
627
|
library_name=None,
|
|
570
|
-
status=
|
|
628
|
+
status=LibraryManager.LibraryFitness.UNUSABLE,
|
|
571
629
|
problems=[LibraryJsonDecodeProblem()],
|
|
572
630
|
result_details=details,
|
|
573
631
|
)
|
|
@@ -576,7 +634,7 @@ class LibraryManager:
|
|
|
576
634
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
577
635
|
library_path=file_path,
|
|
578
636
|
library_name=None,
|
|
579
|
-
status=
|
|
637
|
+
status=LibraryManager.LibraryFitness.UNUSABLE,
|
|
580
638
|
problems=[LibraryLoadExceptionProblem(error_message=str(err))],
|
|
581
639
|
result_details=details,
|
|
582
640
|
)
|
|
@@ -600,7 +658,7 @@ class LibraryManager:
|
|
|
600
658
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
601
659
|
library_path=file_path,
|
|
602
660
|
library_name=library_name,
|
|
603
|
-
status=
|
|
661
|
+
status=LibraryManager.LibraryFitness.UNUSABLE,
|
|
604
662
|
problems=problems,
|
|
605
663
|
result_details=details,
|
|
606
664
|
)
|
|
@@ -609,11 +667,23 @@ class LibraryManager:
|
|
|
609
667
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
610
668
|
library_path=file_path,
|
|
611
669
|
library_name=library_name,
|
|
612
|
-
status=
|
|
670
|
+
status=LibraryManager.LibraryFitness.UNUSABLE,
|
|
613
671
|
problems=[LibrarySchemaExceptionProblem(error_message=str(err))],
|
|
614
672
|
result_details=details,
|
|
615
673
|
)
|
|
616
674
|
|
|
675
|
+
# Make sure the version string is copacetic.
|
|
676
|
+
library_version = library_data.metadata.library_version
|
|
677
|
+
if library_version is None:
|
|
678
|
+
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."
|
|
679
|
+
return LoadLibraryMetadataFromFileResultFailure(
|
|
680
|
+
library_path=file_path,
|
|
681
|
+
library_name=library_data.name,
|
|
682
|
+
status=LibraryManager.LibraryFitness.UNUSABLE,
|
|
683
|
+
problems=[InvalidVersionStringProblem(version_string=str(library_data.metadata.library_version))],
|
|
684
|
+
result_details=details,
|
|
685
|
+
)
|
|
686
|
+
|
|
617
687
|
# Get git remote and ref if this library is in a git repository
|
|
618
688
|
library_dir = json_path.parent.absolute()
|
|
619
689
|
try:
|
|
@@ -659,13 +729,35 @@ class LibraryManager:
|
|
|
659
729
|
else:
|
|
660
730
|
failed_libraries.append(cast("LoadLibraryMetadataFromFileResultFailure", metadata_result))
|
|
661
731
|
|
|
662
|
-
# Generate sandbox library metadata
|
|
663
|
-
|
|
664
|
-
if
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
732
|
+
# Generate sandbox library metadata if configured
|
|
733
|
+
sandbox_library_dir = self._get_sandbox_directory()
|
|
734
|
+
if sandbox_library_dir:
|
|
735
|
+
# Try to load existing JSON first - only scan if load fails
|
|
736
|
+
sandbox_json_path = sandbox_library_dir / LibraryManager.LIBRARY_CONFIG_FILENAME
|
|
737
|
+
sandbox_result = self.load_library_metadata_from_file_request(
|
|
738
|
+
LoadLibraryMetadataFromFileRequest(file_path=str(sandbox_json_path))
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
# If load failed, it either didn't exist or was malformed. Try scanning, which will generate a fresh one.
|
|
742
|
+
if isinstance(sandbox_result, LoadLibraryMetadataFromFileResultFailure):
|
|
743
|
+
scan_result = self.scan_sandbox_directory_request(
|
|
744
|
+
ScanSandboxDirectoryRequest(directory_path=str(sandbox_library_dir))
|
|
745
|
+
)
|
|
746
|
+
# Map scan result to load result for consistency
|
|
747
|
+
if isinstance(scan_result, ScanSandboxDirectoryResultSuccess):
|
|
748
|
+
sandbox_result = LoadLibraryMetadataFromFileResultSuccess(
|
|
749
|
+
library_schema=scan_result.library_schema,
|
|
750
|
+
file_path=str(sandbox_json_path),
|
|
751
|
+
git_remote=None,
|
|
752
|
+
git_ref=None,
|
|
753
|
+
result_details=scan_result.result_details,
|
|
754
|
+
)
|
|
755
|
+
# else: Keep the load failure result
|
|
756
|
+
|
|
757
|
+
if isinstance(sandbox_result, LoadLibraryMetadataFromFileResultSuccess):
|
|
758
|
+
successful_libraries.append(sandbox_result)
|
|
759
|
+
else:
|
|
760
|
+
failed_libraries.append(sandbox_result)
|
|
669
761
|
|
|
670
762
|
details = (
|
|
671
763
|
f"Successfully loaded metadata for {len(successful_libraries)} libraries, {len(failed_libraries)} failed"
|
|
@@ -678,130 +770,305 @@ class LibraryManager:
|
|
|
678
770
|
|
|
679
771
|
def _generate_sandbox_library_metadata(
|
|
680
772
|
self,
|
|
773
|
+
sandbox_directory: Path,
|
|
681
774
|
) -> LoadLibraryMetadataFromFileResultSuccess | LoadLibraryMetadataFromFileResultFailure | None:
|
|
682
775
|
"""Generate sandbox library metadata by scanning Python files without importing them.
|
|
683
776
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
config_mgr = GriptapeNodes.ConfigManager()
|
|
687
|
-
sandbox_library_subdir = config_mgr.get_config_value("sandbox_library_directory")
|
|
688
|
-
if not sandbox_library_subdir:
|
|
689
|
-
logger.debug("No sandbox directory specified in config. Skipping sandbox library metadata generation.")
|
|
690
|
-
return None
|
|
777
|
+
Args:
|
|
778
|
+
sandbox_directory: Path to sandbox directory to scan.
|
|
691
779
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
sandbox_library_dir_as_posix =
|
|
780
|
+
Returns None if no files are found.
|
|
781
|
+
"""
|
|
782
|
+
sandbox_library_dir_as_posix = sandbox_directory.as_posix()
|
|
695
783
|
|
|
696
|
-
if not
|
|
784
|
+
if not sandbox_directory.exists():
|
|
697
785
|
details = "Sandbox directory does not exist. If you wish to create a Sandbox directory to develop custom nodes: in the Griptape Nodes editor, go to Settings -> Libraries and navigate to the Sandbox Settings."
|
|
698
786
|
return LoadLibraryMetadataFromFileResultFailure(
|
|
699
787
|
library_path=sandbox_library_dir_as_posix,
|
|
700
788
|
library_name=LibraryManager.SANDBOX_LIBRARY_NAME,
|
|
701
|
-
status=
|
|
789
|
+
status=LibraryManager.LibraryFitness.MISSING,
|
|
702
790
|
problems=[SandboxDirectoryMissingProblem()],
|
|
703
791
|
result_details=ResultDetails(message=details, level=logging.INFO),
|
|
704
792
|
)
|
|
705
793
|
|
|
706
|
-
sandbox_node_candidates = self._find_files_in_dir(directory=
|
|
794
|
+
sandbox_node_candidates = self._find_files_in_dir(directory=sandbox_directory, extension=".py")
|
|
707
795
|
if not sandbox_node_candidates:
|
|
708
796
|
logger.debug(
|
|
709
|
-
"No candidate files found in sandbox directory '%s'.
|
|
710
|
-
|
|
797
|
+
"No candidate files found in sandbox directory '%s'. Creating empty sandbox library metadata.",
|
|
798
|
+
sandbox_directory,
|
|
711
799
|
)
|
|
712
|
-
|
|
800
|
+
# Continue with empty list - create valid schema with 0 nodes
|
|
801
|
+
sandbox_node_candidates = []
|
|
802
|
+
|
|
803
|
+
# Try to load existing library JSON for smart merging
|
|
804
|
+
json_path = sandbox_directory / LibraryManager.LIBRARY_CONFIG_FILENAME
|
|
805
|
+
metadata_result = self.load_library_metadata_from_file_request(
|
|
806
|
+
LoadLibraryMetadataFromFileRequest(file_path=str(json_path))
|
|
807
|
+
)
|
|
713
808
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
node_metadata = NodeMetadata(
|
|
724
|
-
category="Griptape Nodes Sandbox",
|
|
725
|
-
description=f"'{file_name}' may contain one or more nodes defined in this candidate file.",
|
|
726
|
-
display_name=file_name,
|
|
727
|
-
icon="square-dashed",
|
|
728
|
-
color=None,
|
|
809
|
+
existing_schema = None
|
|
810
|
+
if isinstance(metadata_result, LoadLibraryMetadataFromFileResultSuccess):
|
|
811
|
+
existing_schema = metadata_result.library_schema
|
|
812
|
+
logger.debug("Loaded existing sandbox library JSON from '%s'", json_path)
|
|
813
|
+
else:
|
|
814
|
+
logger.debug(
|
|
815
|
+
"No existing sandbox library JSON or failed to load from '%s': %s. Will generate fresh schema.",
|
|
816
|
+
json_path,
|
|
817
|
+
metadata_result.result_details,
|
|
729
818
|
)
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
819
|
+
|
|
820
|
+
if existing_schema is not None:
|
|
821
|
+
# Smart merge: preserve existing customizations, add new files, remove deleted files
|
|
822
|
+
logger.debug(
|
|
823
|
+
"Merging existing sandbox library JSON with discovered files in sandbox directory '%s'",
|
|
824
|
+
sandbox_directory,
|
|
825
|
+
)
|
|
826
|
+
node_definitions = self._merge_sandbox_nodes(
|
|
827
|
+
existing_schema=existing_schema,
|
|
828
|
+
discovered_files=sandbox_node_candidates,
|
|
829
|
+
sandbox_directory=sandbox_directory,
|
|
734
830
|
)
|
|
735
|
-
node_definitions.append(node_definition)
|
|
736
831
|
|
|
737
|
-
|
|
832
|
+
if not node_definitions:
|
|
833
|
+
logger.debug(
|
|
834
|
+
"No valid node files found after merge in sandbox directory '%s'. Creating empty sandbox library metadata.",
|
|
835
|
+
sandbox_directory,
|
|
836
|
+
)
|
|
837
|
+
# Continue with empty list - create valid schema with 0 nodes
|
|
838
|
+
node_definitions = []
|
|
839
|
+
|
|
840
|
+
# Preserve existing library metadata
|
|
841
|
+
library_name = existing_schema.name
|
|
842
|
+
library_metadata = existing_schema.metadata
|
|
843
|
+
categories = existing_schema.categories
|
|
844
|
+
|
|
845
|
+
# Update schema version to latest
|
|
846
|
+
library_schema_version = LibrarySchema.LATEST_SCHEMA_VERSION
|
|
847
|
+
|
|
848
|
+
else:
|
|
849
|
+
# No existing JSON or it failed to load - generate fresh schema
|
|
738
850
|
logger.debug(
|
|
739
|
-
"
|
|
740
|
-
|
|
851
|
+
"Generating fresh sandbox library schema for sandbox directory '%s'",
|
|
852
|
+
sandbox_directory,
|
|
741
853
|
)
|
|
742
|
-
return None
|
|
743
854
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
855
|
+
# Create placeholder node definitions (original behavior)
|
|
856
|
+
node_definitions = []
|
|
857
|
+
for candidate in sandbox_node_candidates:
|
|
858
|
+
# Use placeholder class name to make it obvious when discovery hasn't run yet
|
|
859
|
+
class_name = self.UNRESOLVED_SANDBOX_CLASS_NAME
|
|
860
|
+
file_name = candidate.name
|
|
861
|
+
|
|
862
|
+
# Create a placeholder node definition - we can't get the actual class metadata
|
|
863
|
+
# without importing, so we use defaults
|
|
864
|
+
node_metadata = NodeMetadata(
|
|
865
|
+
category=self.SANDBOX_CATEGORY_NAME,
|
|
866
|
+
description=f"'{file_name}' may contain one or more nodes defined in this candidate file.",
|
|
867
|
+
display_name=file_name,
|
|
868
|
+
icon="square-dashed",
|
|
869
|
+
color=None,
|
|
870
|
+
)
|
|
871
|
+
node_definition = NodeDefinition(
|
|
872
|
+
class_name=class_name,
|
|
873
|
+
file_path=str(candidate.relative_to(sandbox_directory)),
|
|
874
|
+
metadata=node_metadata,
|
|
875
|
+
)
|
|
876
|
+
node_definitions.append(node_definition)
|
|
751
877
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
878
|
+
if not node_definitions:
|
|
879
|
+
logger.debug(
|
|
880
|
+
"No valid node files found in sandbox directory '%s'. Creating empty sandbox library metadata.",
|
|
881
|
+
sandbox_directory,
|
|
882
|
+
)
|
|
883
|
+
# Continue with empty list - create valid schema with 0 nodes
|
|
884
|
+
node_definitions = []
|
|
885
|
+
|
|
886
|
+
# Create default metadata
|
|
887
|
+
sandbox_category = CategoryDefinition(
|
|
888
|
+
title="Sandbox",
|
|
889
|
+
description=f"Nodes loaded from the {LibraryManager.SANDBOX_LIBRARY_NAME}.",
|
|
890
|
+
color="#c7621a",
|
|
891
|
+
icon="Folder",
|
|
761
892
|
)
|
|
762
893
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
894
|
+
engine_version = GriptapeNodes().handle_engine_version_request(request=GetEngineVersionRequest())
|
|
895
|
+
if not isinstance(engine_version, GetEngineVersionResultSuccess):
|
|
896
|
+
details = "Could not get engine version for sandbox library generation."
|
|
897
|
+
return LoadLibraryMetadataFromFileResultFailure(
|
|
898
|
+
library_path=sandbox_library_dir_as_posix,
|
|
899
|
+
library_name=LibraryManager.SANDBOX_LIBRARY_NAME,
|
|
900
|
+
status=LibraryManager.LibraryFitness.UNUSABLE,
|
|
901
|
+
problems=[EngineVersionErrorProblem()],
|
|
902
|
+
result_details=details,
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
engine_version_str = f"{engine_version.major}.{engine_version.minor}.{engine_version.patch}"
|
|
906
|
+
library_metadata = LibraryMetadata(
|
|
907
|
+
author="Author needs to be specified when library is published.",
|
|
908
|
+
description="Nodes loaded from the sandbox library.",
|
|
909
|
+
library_version=engine_version_str,
|
|
910
|
+
engine_version=engine_version_str,
|
|
911
|
+
tags=["sandbox"],
|
|
912
|
+
is_griptape_nodes_searchable=False,
|
|
913
|
+
)
|
|
914
|
+
categories = [
|
|
915
|
+
{self.SANDBOX_CATEGORY_NAME: sandbox_category},
|
|
916
|
+
]
|
|
917
|
+
library_name = LibraryManager.SANDBOX_LIBRARY_NAME
|
|
918
|
+
library_schema_version = LibrarySchema.LATEST_SCHEMA_VERSION
|
|
919
|
+
|
|
920
|
+
# Create the library schema (now using variables set by either path)
|
|
775
921
|
library_schema = LibrarySchema(
|
|
776
|
-
name=
|
|
777
|
-
library_schema_version=
|
|
922
|
+
name=library_name,
|
|
923
|
+
library_schema_version=library_schema_version,
|
|
778
924
|
metadata=library_metadata,
|
|
779
925
|
categories=categories,
|
|
780
926
|
nodes=node_definitions,
|
|
781
927
|
)
|
|
782
928
|
|
|
783
|
-
#
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
except GitRemoteError as e:
|
|
787
|
-
logger.debug("Failed to get git remote for sandbox library %s: %s", sandbox_library_dir, e)
|
|
788
|
-
git_remote = None
|
|
929
|
+
# Sandbox libraries are never git repositories - always set to None
|
|
930
|
+
git_remote = None
|
|
931
|
+
git_ref = None
|
|
789
932
|
|
|
790
|
-
|
|
791
|
-
git_ref = get_current_ref(sandbox_library_dir)
|
|
792
|
-
except GitRefError as e:
|
|
793
|
-
logger.debug("Failed to get git ref for sandbox library %s: %s", sandbox_library_dir, e)
|
|
794
|
-
git_ref = None
|
|
795
|
-
|
|
796
|
-
details = f"Successfully generated sandbox library metadata with {len(node_definitions)} nodes from {sandbox_library_dir}"
|
|
933
|
+
details = f"Successfully generated sandbox library metadata with {len(node_definitions)} nodes from {sandbox_directory}"
|
|
797
934
|
return LoadLibraryMetadataFromFileResultSuccess(
|
|
798
935
|
library_schema=library_schema,
|
|
799
|
-
file_path=str(
|
|
936
|
+
file_path=str(sandbox_directory),
|
|
800
937
|
git_remote=git_remote,
|
|
801
938
|
git_ref=git_ref,
|
|
802
939
|
result_details=details,
|
|
803
940
|
)
|
|
804
941
|
|
|
942
|
+
def _merge_sandbox_nodes(
|
|
943
|
+
self,
|
|
944
|
+
existing_schema: LibrarySchema,
|
|
945
|
+
discovered_files: list[Path],
|
|
946
|
+
sandbox_directory: Path,
|
|
947
|
+
) -> list[NodeDefinition]:
|
|
948
|
+
"""Merge existing node definitions with newly discovered files.
|
|
949
|
+
|
|
950
|
+
Args:
|
|
951
|
+
existing_schema: Previously saved library schema
|
|
952
|
+
discovered_files: List of .py files found in sandbox directory
|
|
953
|
+
sandbox_directory: Path to sandbox directory for computing relative paths
|
|
954
|
+
|
|
955
|
+
Returns:
|
|
956
|
+
Merged list of NodeDefinitions
|
|
957
|
+
"""
|
|
958
|
+
# Create mapping of discovered files for quick lookup (use absolute resolved paths)
|
|
959
|
+
discovered_file_paths = {str(f.resolve()): f for f in discovered_files}
|
|
960
|
+
|
|
961
|
+
# Keep existing nodes that still have corresponding files
|
|
962
|
+
merged_nodes = []
|
|
963
|
+
existing_file_paths = set()
|
|
964
|
+
|
|
965
|
+
for existing_node in existing_schema.nodes:
|
|
966
|
+
# Resolve the file path to absolute for comparison
|
|
967
|
+
try:
|
|
968
|
+
existing_file_path = str(Path(existing_node.file_path).resolve())
|
|
969
|
+
except Exception as e:
|
|
970
|
+
logger.warning(
|
|
971
|
+
"Could not resolve path for existing node '%s' at '%s': %s. Skipping.",
|
|
972
|
+
existing_node.class_name,
|
|
973
|
+
existing_node.file_path,
|
|
974
|
+
e,
|
|
975
|
+
)
|
|
976
|
+
continue
|
|
977
|
+
|
|
978
|
+
# Keep node if file still exists
|
|
979
|
+
if existing_file_path in discovered_file_paths:
|
|
980
|
+
merged_nodes.append(existing_node)
|
|
981
|
+
existing_file_paths.add(existing_file_path)
|
|
982
|
+
logger.debug(
|
|
983
|
+
"Preserved existing sandbox node definition: %s (%s)",
|
|
984
|
+
existing_node.class_name,
|
|
985
|
+
existing_node.file_path,
|
|
986
|
+
)
|
|
987
|
+
else:
|
|
988
|
+
logger.debug(
|
|
989
|
+
"Removing sandbox node '%s' - file no longer exists: %s",
|
|
990
|
+
existing_node.class_name,
|
|
991
|
+
existing_node.file_path,
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
# Add new files as placeholder nodes
|
|
995
|
+
for discovered_file in discovered_files:
|
|
996
|
+
discovered_file_path = str(discovered_file.resolve())
|
|
997
|
+
|
|
998
|
+
if discovered_file_path not in existing_file_paths:
|
|
999
|
+
# Create placeholder node definition for new file
|
|
1000
|
+
class_name = self.UNRESOLVED_SANDBOX_CLASS_NAME
|
|
1001
|
+
file_name = discovered_file.name
|
|
1002
|
+
|
|
1003
|
+
node_metadata = NodeMetadata(
|
|
1004
|
+
category=self.SANDBOX_CATEGORY_NAME,
|
|
1005
|
+
description=f"'{file_name}' may contain one or more nodes defined in this candidate file.",
|
|
1006
|
+
display_name=file_name,
|
|
1007
|
+
icon="square-dashed",
|
|
1008
|
+
color=None,
|
|
1009
|
+
)
|
|
1010
|
+
node_definition = NodeDefinition(
|
|
1011
|
+
class_name=class_name,
|
|
1012
|
+
file_path=str(discovered_file.relative_to(sandbox_directory)),
|
|
1013
|
+
metadata=node_metadata,
|
|
1014
|
+
)
|
|
1015
|
+
merged_nodes.append(node_definition)
|
|
1016
|
+
logger.debug(
|
|
1017
|
+
"Added new placeholder sandbox node: %s (%s)",
|
|
1018
|
+
file_name,
|
|
1019
|
+
discovered_file.relative_to(sandbox_directory),
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
return merged_nodes
|
|
1023
|
+
|
|
1024
|
+
def _get_sandbox_directory(self) -> Path | None:
|
|
1025
|
+
"""Get the configured sandbox directory path.
|
|
1026
|
+
|
|
1027
|
+
Returns:
|
|
1028
|
+
Path to sandbox directory if configured and exists, None otherwise.
|
|
1029
|
+
"""
|
|
1030
|
+
config_mgr = GriptapeNodes.ConfigManager()
|
|
1031
|
+
sandbox_library_subdir = config_mgr.get_config_value("sandbox_library_directory")
|
|
1032
|
+
if not sandbox_library_subdir:
|
|
1033
|
+
return None
|
|
1034
|
+
|
|
1035
|
+
sandbox_library_dir = config_mgr.workspace_path / sandbox_library_subdir
|
|
1036
|
+
if not sandbox_library_dir.exists():
|
|
1037
|
+
return None
|
|
1038
|
+
|
|
1039
|
+
return sandbox_library_dir
|
|
1040
|
+
|
|
1041
|
+
def scan_sandbox_directory_request(
|
|
1042
|
+
self,
|
|
1043
|
+
request: ScanSandboxDirectoryRequest,
|
|
1044
|
+
) -> ScanSandboxDirectoryResultSuccess | ScanSandboxDirectoryResultFailure:
|
|
1045
|
+
"""Handle ScanSandboxDirectoryRequest.
|
|
1046
|
+
|
|
1047
|
+
Scans specified sandbox directory and generates/merges library metadata.
|
|
1048
|
+
"""
|
|
1049
|
+
sandbox_directory = Path(request.directory_path)
|
|
1050
|
+
|
|
1051
|
+
# Generate/merge library metadata
|
|
1052
|
+
result = self._generate_sandbox_library_metadata(sandbox_directory=sandbox_directory)
|
|
1053
|
+
|
|
1054
|
+
# Note: result should never be None after Step 1 fix, but handle defensively
|
|
1055
|
+
if result is None:
|
|
1056
|
+
details = f"Internal error: _generate_sandbox_library_metadata returned None for {sandbox_directory}"
|
|
1057
|
+
return ScanSandboxDirectoryResultFailure(result_details=ResultDetails(message=details, level=logging.ERROR))
|
|
1058
|
+
|
|
1059
|
+
if isinstance(result, LoadLibraryMetadataFromFileResultFailure):
|
|
1060
|
+
# Failure during generation
|
|
1061
|
+
return ScanSandboxDirectoryResultFailure(result_details=result.result_details)
|
|
1062
|
+
|
|
1063
|
+
# Success
|
|
1064
|
+
return ScanSandboxDirectoryResultSuccess(
|
|
1065
|
+
library_schema=result.library_schema,
|
|
1066
|
+
result_details=ResultDetails(
|
|
1067
|
+
message=f"Scanned sandbox directory: {len(result.library_schema.nodes)} node definitions",
|
|
1068
|
+
level=logging.INFO,
|
|
1069
|
+
),
|
|
1070
|
+
)
|
|
1071
|
+
|
|
805
1072
|
def get_node_metadata_from_library_request(self, request: GetNodeMetadataFromLibraryRequest) -> ResultPayload:
|
|
806
1073
|
# Does this library exist?
|
|
807
1074
|
try:
|
|
@@ -842,206 +1109,443 @@ class LibraryManager:
|
|
|
842
1109
|
)
|
|
843
1110
|
return result
|
|
844
1111
|
|
|
845
|
-
async def register_library_from_file_request(self, request: RegisterLibraryFromFileRequest) -> ResultPayload: # noqa:
|
|
846
|
-
|
|
1112
|
+
async def register_library_from_file_request(self, request: RegisterLibraryFromFileRequest) -> ResultPayload: # noqa: PLR0911 (result determination needs returns)
|
|
1113
|
+
"""Register a library by name or path, progressing through all lifecycle phases.
|
|
847
1114
|
|
|
848
|
-
|
|
849
|
-
|
|
1115
|
+
Supports loading by library_name OR file_path (mutually exclusive), with optional
|
|
1116
|
+
discovery integration. Creates LibraryInfo if not already tracked.
|
|
850
1117
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
library_path=file_path,
|
|
855
|
-
library_name=None,
|
|
856
|
-
status=LibraryStatus.MISSING,
|
|
857
|
-
problems=[LibraryNotFoundProblem(library_path=file_path)],
|
|
858
|
-
)
|
|
859
|
-
details = f"Attempted to load Library JSON file. Failed because no file could be found at the specified path: {json_path}"
|
|
860
|
-
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
861
|
-
|
|
862
|
-
# Use the new metadata loading functionality
|
|
863
|
-
metadata_request = LoadLibraryMetadataFromFileRequest(file_path=file_path)
|
|
864
|
-
metadata_result = self.load_library_metadata_from_file_request(metadata_request)
|
|
1118
|
+
Args:
|
|
1119
|
+
request: RegisterLibraryFromFileRequest containing library_name OR file_path,
|
|
1120
|
+
perform_discovery_if_not_found, and load_as_default_library
|
|
865
1121
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1122
|
+
Returns:
|
|
1123
|
+
RegisterLibraryFromFileResultSuccess if loaded, RegisterLibraryFromFileResultFailure otherwise
|
|
1124
|
+
"""
|
|
1125
|
+
# Phase 1: Establish prerequisites
|
|
1126
|
+
prereq_result = await self._establish_register_library_prerequisites(request)
|
|
869
1127
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
status=failure_result.status,
|
|
874
|
-
problems=failure_result.problems,
|
|
875
|
-
)
|
|
876
|
-
return RegisterLibraryFromFileResultFailure(result_details=str(failure_result.result_details))
|
|
1128
|
+
# FAILURE CHECK FIRST
|
|
1129
|
+
if isinstance(prereq_result, RegisterLibraryFromFileResultFailure):
|
|
1130
|
+
return prereq_result
|
|
877
1131
|
|
|
878
|
-
#
|
|
879
|
-
|
|
1132
|
+
# SUCCESS CHECK (library already loaded)
|
|
1133
|
+
if isinstance(prereq_result, RegisterLibraryFromFileResultSuccess):
|
|
1134
|
+
return prereq_result
|
|
880
1135
|
|
|
881
|
-
#
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
|
|
885
|
-
library_path=file_path,
|
|
886
|
-
library_name=library_data.name,
|
|
887
|
-
status=LibraryStatus.UNUSABLE,
|
|
888
|
-
problems=[InvalidVersionStringProblem(version_string=str(library_data.metadata.library_version))],
|
|
889
|
-
)
|
|
890
|
-
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."
|
|
891
|
-
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
1136
|
+
# Extract prerequisites
|
|
1137
|
+
library_info = prereq_result.library_info
|
|
1138
|
+
file_path = prereq_result.file_path
|
|
892
1139
|
|
|
893
|
-
#
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1140
|
+
# Phase 2: Progress through lifecycle phases
|
|
1141
|
+
progression_result = await self._progress_library_through_lifecycle(
|
|
1142
|
+
library_info=library_info, file_path=file_path, request=request
|
|
1143
|
+
)
|
|
897
1144
|
|
|
898
|
-
#
|
|
899
|
-
if
|
|
900
|
-
|
|
1145
|
+
# FAILURE CHECK
|
|
1146
|
+
if isinstance(progression_result, RegisterLibraryFromFileResultFailure):
|
|
1147
|
+
return progression_result
|
|
901
1148
|
|
|
902
|
-
|
|
903
|
-
|
|
1149
|
+
# Phase 3: Return appropriate result based on fitness
|
|
1150
|
+
# At this point, library_name must be set (it's set during METADATA_LOADED phase)
|
|
1151
|
+
if library_info.library_name is None:
|
|
1152
|
+
details = "Library loaded but library_name was not set during metadata loading"
|
|
1153
|
+
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
904
1154
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1155
|
+
match library_info.fitness:
|
|
1156
|
+
case LibraryManager.LibraryFitness.GOOD:
|
|
1157
|
+
details = f"Successfully loaded Library '{library_info.library_name}' from JSON file at {file_path}"
|
|
1158
|
+
return RegisterLibraryFromFileResultSuccess(
|
|
1159
|
+
library_name=library_info.library_name,
|
|
1160
|
+
result_details=ResultDetails(message=details, level=logging.INFO),
|
|
1161
|
+
)
|
|
1162
|
+
case LibraryManager.LibraryFitness.FLAWED:
|
|
1163
|
+
details = f"Successfully loaded Library JSON file from '{file_path}', but one or more nodes failed to load. Check the log for more details."
|
|
1164
|
+
return RegisterLibraryFromFileResultSuccess(
|
|
1165
|
+
library_name=library_info.library_name,
|
|
1166
|
+
result_details=ResultDetails(message=details, level=logging.WARNING),
|
|
908
1167
|
)
|
|
1168
|
+
case LibraryManager.LibraryFitness.UNUSABLE:
|
|
1169
|
+
details = f"Attempted to load Library JSON file from '{file_path}'. Failed because no nodes were loaded. Check the log for more details."
|
|
1170
|
+
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
1171
|
+
case _:
|
|
1172
|
+
details = f"Attempted to load Library JSON file from '{file_path}'. Failed because an unknown/unexpected fitness '{library_info.fitness}' was returned."
|
|
909
1173
|
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
910
1174
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1175
|
+
async def _establish_register_library_prerequisites( # noqa: C901, PLR0911, PLR0912 (prerequisite validation needs branches)
|
|
1176
|
+
self, request: RegisterLibraryFromFileRequest
|
|
1177
|
+
) -> (
|
|
1178
|
+
LibraryManager.RegisterLibraryPrerequisites
|
|
1179
|
+
| RegisterLibraryFromFileResultSuccess
|
|
1180
|
+
| RegisterLibraryFromFileResultFailure
|
|
1181
|
+
):
|
|
1182
|
+
"""Validate request and establish library identity.
|
|
1183
|
+
|
|
1184
|
+
Returns:
|
|
1185
|
+
RegisterLibraryPrerequisites: Ready for lifecycle progression
|
|
1186
|
+
RegisterLibraryFromFileResultSuccess: Library already loaded (early exit)
|
|
1187
|
+
RegisterLibraryFromFileResultFailure: Validation or lookup failed
|
|
1188
|
+
"""
|
|
1189
|
+
# Validate request has either library_name or file_path (but not both)
|
|
1190
|
+
if not request.library_name and not request.file_path:
|
|
1191
|
+
return RegisterLibraryFromFileResultFailure(
|
|
1192
|
+
result_details="Attempted to register a library. Failed because neither library name nor file path were specified."
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
if request.library_name and request.file_path:
|
|
1196
|
+
return RegisterLibraryFromFileResultFailure(
|
|
1197
|
+
result_details="Attempted to register a library. Failed because both library name and file path were specified."
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
library_name = request.library_name
|
|
1201
|
+
file_path = request.file_path
|
|
1202
|
+
|
|
1203
|
+
# If file_path provided but not library_name, load metadata to get the name
|
|
1204
|
+
if file_path and not library_name:
|
|
1205
|
+
lib_info = self._library_file_path_to_info.get(file_path)
|
|
1206
|
+
|
|
1207
|
+
# If we don't have LibraryInfo yet, load metadata to get the name
|
|
1208
|
+
if not lib_info or not lib_info.library_name:
|
|
1209
|
+
metadata_result = self.load_library_metadata_from_file_request(
|
|
1210
|
+
LoadLibraryMetadataFromFileRequest(file_path=file_path)
|
|
916
1211
|
)
|
|
917
1212
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1213
|
+
if isinstance(metadata_result, LoadLibraryMetadataFromFileResultFailure):
|
|
1214
|
+
return RegisterLibraryFromFileResultFailure(result_details=metadata_result.result_details)
|
|
1215
|
+
|
|
1216
|
+
library_name = metadata_result.library_schema.name
|
|
1217
|
+
|
|
1218
|
+
# Update or create LibraryInfo
|
|
1219
|
+
if lib_info:
|
|
1220
|
+
lib_info.library_name = library_name
|
|
1221
|
+
lib_info.library_version = metadata_result.library_schema.metadata.library_version
|
|
1222
|
+
lib_info.lifecycle_state = LibraryManager.LibraryLifecycleState.METADATA_LOADED
|
|
1223
|
+
else:
|
|
1224
|
+
# Create new LibraryInfo since it doesn't exist yet
|
|
1225
|
+
lib_info = LibraryManager.LibraryInfo(
|
|
1226
|
+
lifecycle_state=LibraryManager.LibraryLifecycleState.METADATA_LOADED,
|
|
1227
|
+
library_path=file_path,
|
|
1228
|
+
is_sandbox=False,
|
|
1229
|
+
library_name=library_name,
|
|
1230
|
+
library_version=metadata_result.library_schema.metadata.library_version,
|
|
1231
|
+
fitness=LibraryManager.LibraryFitness.NOT_EVALUATED,
|
|
1232
|
+
problems=[],
|
|
926
1233
|
)
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1234
|
+
self._library_file_path_to_info[file_path] = lib_info
|
|
1235
|
+
else:
|
|
1236
|
+
library_name = lib_info.library_name
|
|
930
1237
|
|
|
931
|
-
#
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
try:
|
|
935
|
-
advanced_library_instance = self._load_advanced_library_module(
|
|
936
|
-
library_data=library_data,
|
|
937
|
-
base_dir=base_dir,
|
|
938
|
-
)
|
|
939
|
-
except Exception as err:
|
|
940
|
-
self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
|
|
941
|
-
library_path=file_path,
|
|
942
|
-
library_name=library_data.name,
|
|
943
|
-
library_version=library_version,
|
|
944
|
-
status=LibraryStatus.UNUSABLE,
|
|
945
|
-
problems=[
|
|
946
|
-
AdvancedLibraryLoadFailureProblem(
|
|
947
|
-
advanced_library_path=library_data.advanced_library_path, error_message=str(err)
|
|
948
|
-
)
|
|
949
|
-
],
|
|
950
|
-
)
|
|
951
|
-
details = f"Attempted to load Library '{library_data.name}' from '{json_path}'. Failed to load Advanced Library module: {err}"
|
|
952
|
-
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
1238
|
+
# At this point, library_name must be set (either from request or from metadata)
|
|
1239
|
+
if not library_name:
|
|
1240
|
+
return RegisterLibraryFromFileResultFailure(result_details="Failed to determine library name")
|
|
953
1241
|
|
|
954
|
-
#
|
|
1242
|
+
# Check if already loaded in registry
|
|
955
1243
|
try:
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
advanced_library=advanced_library_instance,
|
|
961
|
-
)
|
|
962
|
-
|
|
963
|
-
except KeyError as err:
|
|
964
|
-
# Library already exists
|
|
965
|
-
self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
|
|
966
|
-
library_path=file_path,
|
|
967
|
-
library_name=library_data.name,
|
|
968
|
-
library_version=library_version,
|
|
969
|
-
status=LibraryStatus.UNUSABLE,
|
|
970
|
-
problems=[DuplicateLibraryProblem()],
|
|
1244
|
+
LibraryRegistry.get_library(name=library_name)
|
|
1245
|
+
return RegisterLibraryFromFileResultSuccess(
|
|
1246
|
+
library_name=library_name,
|
|
1247
|
+
result_details=f"Library '{library_name}' already loaded",
|
|
971
1248
|
)
|
|
972
|
-
|
|
973
|
-
|
|
1249
|
+
except KeyError:
|
|
1250
|
+
pass # Not loaded, continue
|
|
1251
|
+
|
|
1252
|
+
# Look up LibraryInfo by library_name (supports lazy loading)
|
|
1253
|
+
library_info = self.get_library_info_by_library_name(library_name)
|
|
1254
|
+
|
|
1255
|
+
# If not found and discovery is allowed, try discovery
|
|
1256
|
+
if library_info is None and request.perform_discovery_if_not_found:
|
|
1257
|
+
discover_result = self.discover_libraries_request(DiscoverLibrariesRequest())
|
|
1258
|
+
if isinstance(discover_result, DiscoverLibrariesResultSuccess):
|
|
1259
|
+
library_info = self.get_library_info_by_library_name(library_name)
|
|
1260
|
+
|
|
1261
|
+
# If still not found, fail
|
|
1262
|
+
if library_info is None:
|
|
1263
|
+
details = f"Library '{library_name}' not found"
|
|
1264
|
+
if request.perform_discovery_if_not_found:
|
|
1265
|
+
details += " (discovery was attempted)"
|
|
974
1266
|
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
975
1267
|
|
|
976
|
-
|
|
977
|
-
# Record all problems that occurred
|
|
978
|
-
problems = []
|
|
1268
|
+
file_path = library_info.library_path
|
|
979
1269
|
|
|
980
|
-
# Check
|
|
981
|
-
if
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1270
|
+
# Check if already loaded in registry (by name if we have it)
|
|
1271
|
+
if library_info.library_name:
|
|
1272
|
+
try:
|
|
1273
|
+
LibraryRegistry.get_library(name=library_info.library_name)
|
|
1274
|
+
except KeyError:
|
|
1275
|
+
# Library not in registry, continue with loading
|
|
1276
|
+
pass
|
|
1277
|
+
else:
|
|
1278
|
+
# Already loaded and good to go
|
|
1279
|
+
return RegisterLibraryFromFileResultSuccess(
|
|
1280
|
+
library_name=library_info.library_name,
|
|
1281
|
+
result_details=f"Library '{library_info.library_name}' already loaded",
|
|
988
1282
|
)
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1283
|
+
|
|
1284
|
+
# Prerequisites established - ready for lifecycle progression
|
|
1285
|
+
return LibraryManager.RegisterLibraryPrerequisites(library_info=library_info, file_path=file_path)
|
|
1286
|
+
|
|
1287
|
+
async def _progress_library_through_lifecycle( # noqa: C901, PLR0911, PLR0912, PLR0915 (lifecycle state machine needs branches/statements/returns)
|
|
1288
|
+
self,
|
|
1289
|
+
library_info: LibraryManager.LibraryInfo,
|
|
1290
|
+
file_path: str,
|
|
1291
|
+
request: RegisterLibraryFromFileRequest,
|
|
1292
|
+
) -> None | RegisterLibraryFromFileResultFailure:
|
|
1293
|
+
"""Progress library through lifecycle states until LOADED.
|
|
1294
|
+
|
|
1295
|
+
Advances library_info through states: DISCOVERED → METADATA_LOADED →
|
|
1296
|
+
EVALUATED → DEPENDENCIES_INSTALLED → LOADED.
|
|
1297
|
+
|
|
1298
|
+
Modifies library_info in place as it progresses through states.
|
|
1299
|
+
|
|
1300
|
+
Returns:
|
|
1301
|
+
None: Successfully progressed to LOADED state
|
|
1302
|
+
RegisterLibraryFromFileResultFailure: Failed during progression
|
|
1303
|
+
"""
|
|
1304
|
+
while True:
|
|
1305
|
+
current_state = library_info.lifecycle_state
|
|
1306
|
+
|
|
1307
|
+
match current_state:
|
|
1308
|
+
case LibraryManager.LibraryLifecycleState.LOADED:
|
|
1309
|
+
# Terminal state: inconsistent (marked LOADED but not in registry)
|
|
1310
|
+
details = f"Library '{library_info.library_name}' marked as LOADED but not in registry"
|
|
1311
|
+
self._library_file_path_to_info[library_info.library_path] = library_info
|
|
1312
|
+
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
1313
|
+
|
|
1314
|
+
case LibraryManager.LibraryLifecycleState.FAILURE:
|
|
1315
|
+
# Terminal state: failure
|
|
1316
|
+
details = f"Library '{library_info.library_name}' is in FAILURE state and cannot be loaded"
|
|
1317
|
+
self._library_file_path_to_info[library_info.library_path] = library_info
|
|
1318
|
+
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
1319
|
+
|
|
1320
|
+
case LibraryManager.LibraryLifecycleState.DISCOVERED:
|
|
1321
|
+
# DISCOVERED → METADATA_LOADED
|
|
1322
|
+
# All libraries (including sandbox) load metadata from JSON file
|
|
1323
|
+
metadata_result = self.load_library_metadata_from_file_request(
|
|
1324
|
+
LoadLibraryMetadataFromFileRequest(file_path=library_info.library_path)
|
|
994
1325
|
)
|
|
995
|
-
|
|
996
|
-
if
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1326
|
+
|
|
1327
|
+
if isinstance(metadata_result, LoadLibraryMetadataFromFileResultFailure):
|
|
1328
|
+
self._library_file_path_to_info[library_info.library_path] = library_info
|
|
1329
|
+
return RegisterLibraryFromFileResultFailure(result_details=metadata_result.result_details)
|
|
1330
|
+
|
|
1331
|
+
# Update library_info with metadata results
|
|
1332
|
+
library_info.library_name = metadata_result.library_schema.name
|
|
1333
|
+
library_info.library_version = metadata_result.library_schema.metadata.library_version
|
|
1334
|
+
library_info.lifecycle_state = LibraryManager.LibraryLifecycleState.METADATA_LOADED
|
|
1335
|
+
|
|
1336
|
+
case LibraryManager.LibraryLifecycleState.METADATA_LOADED:
|
|
1337
|
+
# METADATA_LOADED → EVALUATED
|
|
1338
|
+
# Need to load schema to pass to evaluate request
|
|
1339
|
+
metadata_result = self.load_library_metadata_from_file_request(
|
|
1340
|
+
LoadLibraryMetadataFromFileRequest(file_path=library_info.library_path)
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
if isinstance(metadata_result, LoadLibraryMetadataFromFileResultFailure):
|
|
1344
|
+
self._library_file_path_to_info[library_info.library_path] = library_info
|
|
1345
|
+
return RegisterLibraryFromFileResultFailure(result_details=metadata_result.result_details)
|
|
1346
|
+
|
|
1347
|
+
evaluate_result = self.evaluate_library_fitness_request(
|
|
1348
|
+
EvaluateLibraryFitnessRequest(schema=metadata_result.library_schema)
|
|
1005
1349
|
)
|
|
1006
|
-
|
|
1007
|
-
|
|
1350
|
+
if isinstance(evaluate_result, EvaluateLibraryFitnessResultFailure):
|
|
1351
|
+
self._library_file_path_to_info[library_info.library_path] = library_info
|
|
1352
|
+
return RegisterLibraryFromFileResultFailure(result_details=evaluate_result.result_details)
|
|
1353
|
+
|
|
1354
|
+
# Update library_info with evaluation results
|
|
1355
|
+
library_info.fitness = evaluate_result.fitness
|
|
1356
|
+
library_info.problems.extend(evaluate_result.problems)
|
|
1357
|
+
|
|
1358
|
+
# Check if library requirements are met by the current system
|
|
1359
|
+
library_data = metadata_result.library_schema
|
|
1360
|
+
library_requirements = (
|
|
1361
|
+
library_data.metadata.resources.required
|
|
1362
|
+
if library_data.metadata.resources is not None
|
|
1363
|
+
else None
|
|
1008
1364
|
)
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1365
|
+
if library_requirements is not None:
|
|
1366
|
+
requirements_check_result = self._check_library_requirements(
|
|
1367
|
+
library_requirements, library_data.name
|
|
1368
|
+
)
|
|
1369
|
+
if requirements_check_result is not None:
|
|
1370
|
+
library_info.fitness = LibraryManager.LibraryFitness.UNUSABLE
|
|
1371
|
+
library_info.problems.append(requirements_check_result)
|
|
1372
|
+
library_info.lifecycle_state = LibraryManager.LibraryLifecycleState.FAILURE
|
|
1373
|
+
self._library_file_path_to_info[library_info.library_path] = library_info
|
|
1374
|
+
details = f"Library '{library_data.name}' requirements not met: {library_requirements}"
|
|
1375
|
+
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
1376
|
+
|
|
1377
|
+
library_info.lifecycle_state = LibraryManager.LibraryLifecycleState.EVALUATED
|
|
1378
|
+
|
|
1379
|
+
case LibraryManager.LibraryLifecycleState.EVALUATED:
|
|
1380
|
+
# EVALUATED → DEPENDENCIES_INSTALLED
|
|
1381
|
+
install_result = await self.install_library_dependencies_request(
|
|
1382
|
+
InstallLibraryDependenciesRequest(library_file_path=library_info.library_path)
|
|
1383
|
+
)
|
|
1384
|
+
if isinstance(install_result, InstallLibraryDependenciesResultFailure):
|
|
1385
|
+
self._library_file_path_to_info[library_info.library_path] = library_info
|
|
1386
|
+
return RegisterLibraryFromFileResultFailure(result_details=install_result.result_details)
|
|
1027
1387
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1388
|
+
# Update library_info
|
|
1389
|
+
library_info.lifecycle_state = LibraryManager.LibraryLifecycleState.DEPENDENCIES_INSTALLED
|
|
1390
|
+
|
|
1391
|
+
case LibraryManager.LibraryLifecycleState.DEPENDENCIES_INSTALLED:
|
|
1392
|
+
# DEPENDENCIES_INSTALLED → LOADED
|
|
1393
|
+
|
|
1394
|
+
if not library_info.is_sandbox:
|
|
1395
|
+
# REGULAR LIBRARIES: Standard registration from JSON file
|
|
1396
|
+
# Load metadata and create library
|
|
1397
|
+
metadata_result = self.load_library_metadata_from_file_request(
|
|
1398
|
+
LoadLibraryMetadataFromFileRequest(file_path=library_info.library_path)
|
|
1399
|
+
)
|
|
1400
|
+
|
|
1401
|
+
if isinstance(metadata_result, LoadLibraryMetadataFromFileResultFailure):
|
|
1402
|
+
self._library_file_path_to_info[library_info.library_path] = library_info
|
|
1403
|
+
return RegisterLibraryFromFileResultFailure(result_details=metadata_result.result_details)
|
|
1404
|
+
|
|
1405
|
+
library_data = metadata_result.library_schema
|
|
1406
|
+
json_path = Path(file_path)
|
|
1407
|
+
base_dir = json_path.parent.absolute()
|
|
1408
|
+
|
|
1409
|
+
# Add the directory to the Python path to allow for relative imports
|
|
1410
|
+
sys.path.insert(0, str(base_dir))
|
|
1411
|
+
|
|
1412
|
+
# Add venv site-packages to sys.path if library has dependencies
|
|
1413
|
+
if library_data.metadata.dependencies and library_data.metadata.dependencies.pip_dependencies:
|
|
1414
|
+
venv_path = self._get_library_venv_path(library_data.name, file_path)
|
|
1415
|
+
if venv_path.exists():
|
|
1416
|
+
site_packages = str(
|
|
1417
|
+
Path(
|
|
1418
|
+
sysconfig.get_path(
|
|
1419
|
+
"purelib",
|
|
1420
|
+
vars={"base": str(venv_path), "platbase": str(venv_path)},
|
|
1421
|
+
)
|
|
1422
|
+
)
|
|
1423
|
+
)
|
|
1424
|
+
sys.path.insert(0, site_packages)
|
|
1425
|
+
logger.debug(
|
|
1426
|
+
"Added library '%s' venv to sys.path: %s", library_data.name, site_packages
|
|
1427
|
+
)
|
|
1428
|
+
|
|
1429
|
+
# Load the advanced library module if specified
|
|
1430
|
+
advanced_library_instance = None
|
|
1431
|
+
if library_data.advanced_library_path:
|
|
1432
|
+
try:
|
|
1433
|
+
advanced_library_instance = self._load_advanced_library_module(
|
|
1434
|
+
library_data=library_data,
|
|
1435
|
+
base_dir=base_dir,
|
|
1436
|
+
)
|
|
1437
|
+
except Exception as err:
|
|
1438
|
+
library_info.lifecycle_state = LibraryManager.LibraryLifecycleState.FAILURE
|
|
1439
|
+
library_info.fitness = LibraryManager.LibraryFitness.UNUSABLE
|
|
1440
|
+
library_info.problems.append(
|
|
1441
|
+
AdvancedLibraryLoadFailureProblem(
|
|
1442
|
+
advanced_library_path=library_data.advanced_library_path, error_message=str(err)
|
|
1443
|
+
)
|
|
1444
|
+
)
|
|
1445
|
+
self._library_file_path_to_info[file_path] = library_info
|
|
1446
|
+
details = f"Attempted to load Library '{library_data.name}' from '{json_path}'. Failed to load Advanced Library module: {err}"
|
|
1447
|
+
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
1448
|
+
|
|
1449
|
+
# Create or get the library
|
|
1450
|
+
try:
|
|
1451
|
+
library = LibraryRegistry.generate_new_library(
|
|
1452
|
+
library_data=library_data,
|
|
1453
|
+
mark_as_default_library=request.load_as_default_library,
|
|
1454
|
+
advanced_library=advanced_library_instance,
|
|
1455
|
+
)
|
|
1456
|
+
except KeyError as err:
|
|
1457
|
+
# Library already exists
|
|
1458
|
+
library_info.lifecycle_state = LibraryManager.LibraryLifecycleState.FAILURE
|
|
1459
|
+
library_info.fitness = LibraryManager.LibraryFitness.UNUSABLE
|
|
1460
|
+
library_info.problems.append(DuplicateLibraryProblem())
|
|
1461
|
+
self._library_file_path_to_info[file_path] = library_info
|
|
1462
|
+
details = f"Attempted to load Library JSON file from '{json_path}'. Failed because a Library '{library_data.name}' already exists. Error: {err}."
|
|
1463
|
+
return RegisterLibraryFromFileResultFailure(result_details=details)
|
|
1464
|
+
|
|
1465
|
+
# Check the library's custom config settings
|
|
1466
|
+
if library_data.settings is not None:
|
|
1467
|
+
for library_data_setting in library_data.settings:
|
|
1468
|
+
# Does the category exist?
|
|
1469
|
+
get_category_request = GetConfigCategoryRequest(
|
|
1470
|
+
category=library_data_setting.category,
|
|
1471
|
+
failure_log_level=logging.DEBUG,
|
|
1472
|
+
)
|
|
1473
|
+
get_category_result = GriptapeNodes.handle_request(get_category_request)
|
|
1474
|
+
if not isinstance(get_category_result, GetConfigCategoryResultSuccess):
|
|
1475
|
+
# Create new category
|
|
1476
|
+
create_new_category_request = SetConfigCategoryRequest(
|
|
1477
|
+
category=library_data_setting.category, contents=library_data_setting.contents
|
|
1478
|
+
)
|
|
1479
|
+
create_new_category_result = GriptapeNodes.handle_request(
|
|
1480
|
+
create_new_category_request
|
|
1481
|
+
)
|
|
1482
|
+
if not isinstance(create_new_category_result, SetConfigCategoryResultSuccess):
|
|
1483
|
+
library_info.problems.append(
|
|
1484
|
+
CreateConfigCategoryProblem(category_name=library_data_setting.category)
|
|
1485
|
+
)
|
|
1486
|
+
details = f"Failed attempting to create new config category '{library_data_setting.category}' for library '{library_data.name}'."
|
|
1487
|
+
logger.error(details)
|
|
1488
|
+
continue
|
|
1489
|
+
else:
|
|
1490
|
+
# Merge with existing category
|
|
1491
|
+
existing_category_contents = merge_dicts(
|
|
1492
|
+
library_data_setting.contents,
|
|
1493
|
+
get_category_result.contents,
|
|
1494
|
+
add_keys=True,
|
|
1495
|
+
merge_lists=True,
|
|
1496
|
+
)
|
|
1497
|
+
set_category_request = SetConfigCategoryRequest(
|
|
1498
|
+
category=library_data_setting.category, contents=existing_category_contents
|
|
1499
|
+
)
|
|
1500
|
+
set_category_result = GriptapeNodes.handle_request(set_category_request)
|
|
1501
|
+
if not isinstance(set_category_result, SetConfigCategoryResultSuccess):
|
|
1502
|
+
library_info.problems.append(
|
|
1503
|
+
UpdateConfigCategoryProblem(category_name=library_data_setting.category)
|
|
1504
|
+
)
|
|
1505
|
+
details = f"Failed attempting to update config category '{library_data_setting.category}' for library '{library_data.name}'."
|
|
1506
|
+
logger.error(details)
|
|
1507
|
+
continue
|
|
1508
|
+
|
|
1509
|
+
# Attempt to load nodes from the library (modifies library_info in place)
|
|
1510
|
+
await asyncio.to_thread(
|
|
1511
|
+
self._attempt_load_nodes_from_library,
|
|
1512
|
+
library_data=library_data,
|
|
1513
|
+
library=library,
|
|
1514
|
+
base_dir=base_dir,
|
|
1515
|
+
library_info=library_info,
|
|
1516
|
+
)
|
|
1517
|
+
self._library_file_path_to_info[file_path] = library_info
|
|
1518
|
+
else:
|
|
1519
|
+
# SANDBOX LIBRARIES: Full processing here (discovery + registration)
|
|
1520
|
+
# Load metadata from JSON file (already generated in DISCOVERED → METADATA_LOADED)
|
|
1521
|
+
sandbox_directory = Path(library_info.library_path).parent
|
|
1522
|
+
metadata_result = self.load_library_metadata_from_file_request(
|
|
1523
|
+
LoadLibraryMetadataFromFileRequest(file_path=library_info.library_path)
|
|
1524
|
+
)
|
|
1525
|
+
|
|
1526
|
+
if isinstance(metadata_result, LoadLibraryMetadataFromFileResultFailure):
|
|
1527
|
+
self._library_file_path_to_info[library_info.library_path] = library_info
|
|
1528
|
+
return RegisterLibraryFromFileResultFailure(result_details=metadata_result.result_details)
|
|
1529
|
+
|
|
1530
|
+
# Discover real class names by importing files
|
|
1531
|
+
await self._attempt_generate_sandbox_library_from_schema(
|
|
1532
|
+
library_schema=metadata_result.library_schema,
|
|
1533
|
+
sandbox_directory=str(sandbox_directory),
|
|
1534
|
+
library_info=library_info,
|
|
1535
|
+
)
|
|
1536
|
+
# Function handles registration and updates library_info with problems
|
|
1537
|
+
# lifecycle_state set to LOADED by _attempt_load_nodes_from_library
|
|
1538
|
+
|
|
1539
|
+
# Exit loop after final phase
|
|
1540
|
+
break
|
|
1541
|
+
|
|
1542
|
+
case _:
|
|
1543
|
+
# Unexpected state
|
|
1544
|
+
msg = f"Library '{library_info.library_name}' in unexpected lifecycle state: {current_state}"
|
|
1545
|
+
raise ValueError(msg)
|
|
1546
|
+
|
|
1547
|
+
# Success - progressed to LOADED state
|
|
1548
|
+
return None
|
|
1045
1549
|
|
|
1046
1550
|
async def register_library_from_requirement_specifier_request(
|
|
1047
1551
|
self, request: RegisterLibraryFromRequirementSpecifierRequest
|
|
@@ -1174,6 +1678,106 @@ class LibraryManager:
|
|
|
1174
1678
|
|
|
1175
1679
|
return library_venv_python_path
|
|
1176
1680
|
|
|
1681
|
+
def _check_library_requirements(
|
|
1682
|
+
self, requirements: dict[str, Any], library_name: str
|
|
1683
|
+
) -> IncompatibleRequirementsProblem | None:
|
|
1684
|
+
"""Check if the current system meets the library's resource requirements.
|
|
1685
|
+
|
|
1686
|
+
Args:
|
|
1687
|
+
requirements: Dictionary of requirements in the format used by resource_instance.Requirements
|
|
1688
|
+
library_name: Name of the library being checked (for logging)
|
|
1689
|
+
|
|
1690
|
+
Returns:
|
|
1691
|
+
IncompatibleRequirementsProblem if requirements are not met, None if they are met
|
|
1692
|
+
"""
|
|
1693
|
+
logger.info("Checking requirements for library '%s': %s", library_name, requirements)
|
|
1694
|
+
|
|
1695
|
+
os_keys = {"platform", "arch", "version"}
|
|
1696
|
+
compute_keys = {"compute"}
|
|
1697
|
+
|
|
1698
|
+
os_requirements = {k: v for k, v in requirements.items() if k in os_keys}
|
|
1699
|
+
compute_requirements = {k: v for k, v in requirements.items() if k in compute_keys}
|
|
1700
|
+
|
|
1701
|
+
if os_requirements:
|
|
1702
|
+
list_request = ListCompatibleResourceInstancesRequest(
|
|
1703
|
+
resource_type_name="OSResourceType",
|
|
1704
|
+
requirements=os_requirements,
|
|
1705
|
+
include_locked=True,
|
|
1706
|
+
)
|
|
1707
|
+
result = GriptapeNodes.handle_request(list_request)
|
|
1708
|
+
|
|
1709
|
+
if isinstance(result, ListCompatibleResourceInstancesResultSuccess) and not result.instance_ids:
|
|
1710
|
+
system_capabilities = self._get_system_capabilities()
|
|
1711
|
+
logger.warning(
|
|
1712
|
+
"Library '%s' OS requirements not met. Required: %s, System: %s",
|
|
1713
|
+
library_name,
|
|
1714
|
+
os_requirements,
|
|
1715
|
+
system_capabilities,
|
|
1716
|
+
)
|
|
1717
|
+
return IncompatibleRequirementsProblem(
|
|
1718
|
+
requirements=requirements,
|
|
1719
|
+
system_capabilities=system_capabilities,
|
|
1720
|
+
)
|
|
1721
|
+
|
|
1722
|
+
if compute_requirements:
|
|
1723
|
+
list_request = ListCompatibleResourceInstancesRequest(
|
|
1724
|
+
resource_type_name="ComputeResourceType",
|
|
1725
|
+
requirements=compute_requirements,
|
|
1726
|
+
include_locked=True,
|
|
1727
|
+
)
|
|
1728
|
+
result = GriptapeNodes.handle_request(list_request)
|
|
1729
|
+
|
|
1730
|
+
if isinstance(result, ListCompatibleResourceInstancesResultSuccess) and not result.instance_ids:
|
|
1731
|
+
system_capabilities = self._get_system_capabilities()
|
|
1732
|
+
logger.warning(
|
|
1733
|
+
"Library '%s' compute requirements not met. Required: %s, System: %s",
|
|
1734
|
+
library_name,
|
|
1735
|
+
compute_requirements,
|
|
1736
|
+
system_capabilities,
|
|
1737
|
+
)
|
|
1738
|
+
return IncompatibleRequirementsProblem(
|
|
1739
|
+
requirements=requirements,
|
|
1740
|
+
system_capabilities=system_capabilities,
|
|
1741
|
+
)
|
|
1742
|
+
|
|
1743
|
+
return None
|
|
1744
|
+
|
|
1745
|
+
def _get_system_capabilities(self) -> dict[str, Any]:
|
|
1746
|
+
"""Get the current system's capabilities for error reporting.
|
|
1747
|
+
|
|
1748
|
+
Returns:
|
|
1749
|
+
Dictionary of combined OS and compute capabilities or empty dict if unavailable
|
|
1750
|
+
"""
|
|
1751
|
+
capabilities: dict[str, Any] = {}
|
|
1752
|
+
|
|
1753
|
+
os_list_request = ListCompatibleResourceInstancesRequest(
|
|
1754
|
+
resource_type_name="OSResourceType",
|
|
1755
|
+
requirements=None,
|
|
1756
|
+
include_locked=True,
|
|
1757
|
+
)
|
|
1758
|
+
os_result = GriptapeNodes.handle_request(os_list_request)
|
|
1759
|
+
|
|
1760
|
+
if isinstance(os_result, ListCompatibleResourceInstancesResultSuccess) and os_result.instance_ids:
|
|
1761
|
+
status_request = GetResourceInstanceStatusRequest(instance_id=os_result.instance_ids[0])
|
|
1762
|
+
status_result = GriptapeNodes.handle_request(status_request)
|
|
1763
|
+
if isinstance(status_result, GetResourceInstanceStatusResultSuccess):
|
|
1764
|
+
capabilities.update(status_result.status.capabilities)
|
|
1765
|
+
|
|
1766
|
+
compute_list_request = ListCompatibleResourceInstancesRequest(
|
|
1767
|
+
resource_type_name="ComputeResourceType",
|
|
1768
|
+
requirements=None,
|
|
1769
|
+
include_locked=True,
|
|
1770
|
+
)
|
|
1771
|
+
compute_result = GriptapeNodes.handle_request(compute_list_request)
|
|
1772
|
+
|
|
1773
|
+
if isinstance(compute_result, ListCompatibleResourceInstancesResultSuccess) and compute_result.instance_ids:
|
|
1774
|
+
status_request = GetResourceInstanceStatusRequest(instance_id=compute_result.instance_ids[0])
|
|
1775
|
+
status_result = GriptapeNodes.handle_request(status_request)
|
|
1776
|
+
if isinstance(status_result, GetResourceInstanceStatusResultSuccess):
|
|
1777
|
+
capabilities.update(status_result.status.capabilities)
|
|
1778
|
+
|
|
1779
|
+
return capabilities
|
|
1780
|
+
|
|
1177
1781
|
def _get_library_venv_path(self, library_name: str, library_file_path: str | None = None) -> Path:
|
|
1178
1782
|
"""Get the path to the virtual environment directory for a library.
|
|
1179
1783
|
|
|
@@ -1626,113 +2230,89 @@ class LibraryManager:
|
|
|
1626
2230
|
async def load_all_libraries_from_config(self) -> None:
|
|
1627
2231
|
self._libraries_loading_complete.clear()
|
|
1628
2232
|
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
status=failed_library.status,
|
|
1645
|
-
problems=failed_library.problems,
|
|
1646
|
-
)
|
|
2233
|
+
# Discover all available libraries (config + sandbox)
|
|
2234
|
+
discover_result = self.discover_libraries_request(DiscoverLibrariesRequest())
|
|
2235
|
+
if isinstance(discover_result, DiscoverLibrariesResultFailure):
|
|
2236
|
+
logger.error("Failed to discover libraries: %s", discover_result.result_details)
|
|
2237
|
+
self._libraries_loading_complete.set()
|
|
2238
|
+
return
|
|
2239
|
+
|
|
2240
|
+
# Build list of library paths to load
|
|
2241
|
+
libraries_to_load = []
|
|
2242
|
+
for discovered_lib in discover_result.libraries_discovered:
|
|
2243
|
+
lib_path = str(discovered_lib.path)
|
|
2244
|
+
lib_info = self._library_file_path_to_info.get(lib_path)
|
|
2245
|
+
|
|
2246
|
+
if lib_info:
|
|
2247
|
+
libraries_to_load.append(lib_path)
|
|
1647
2248
|
|
|
1648
|
-
|
|
1649
|
-
|
|
2249
|
+
if not libraries_to_load:
|
|
2250
|
+
logger.info("No libraries found in configuration.")
|
|
2251
|
+
self._libraries_loading_complete.set()
|
|
2252
|
+
return
|
|
1650
2253
|
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
2254
|
+
# Calculate total libraries for progress tracking
|
|
2255
|
+
total_libraries = len(libraries_to_load)
|
|
2256
|
+
|
|
2257
|
+
# Load each discovered library by path (RegisterLibraryFromFileRequest will handle metadata loading)
|
|
2258
|
+
for current_library_index, lib_path in enumerate(libraries_to_load, start=1):
|
|
2259
|
+
# Load the library through unified lifecycle using library_path
|
|
2260
|
+
# RegisterLibraryFromFileRequest will handle metadata loading internally to get library_name
|
|
2261
|
+
load_result = await self.register_library_from_file_request(
|
|
2262
|
+
RegisterLibraryFromFileRequest(
|
|
2263
|
+
file_path=lib_path,
|
|
2264
|
+
load_as_default_library=False,
|
|
2265
|
+
)
|
|
2266
|
+
)
|
|
1654
2267
|
|
|
1655
|
-
|
|
2268
|
+
# Handle failure case first
|
|
2269
|
+
if isinstance(load_result, RegisterLibraryFromFileResultFailure):
|
|
2270
|
+
logger.warning("Failed to load library at '%s': %s", lib_path, load_result.result_details)
|
|
2271
|
+
error_message = (
|
|
2272
|
+
load_result.result_details.result_details[0].message
|
|
2273
|
+
if isinstance(load_result.result_details, ResultDetails)
|
|
2274
|
+
else str(load_result.result_details)
|
|
2275
|
+
)
|
|
1656
2276
|
GriptapeNodes.EventManager().put_event(
|
|
1657
2277
|
AppEvent(
|
|
1658
2278
|
payload=EngineInitializationProgress(
|
|
1659
2279
|
phase=InitializationPhase.LIBRARIES,
|
|
1660
|
-
item_name=library_name
|
|
1661
|
-
status=InitializationStatus.
|
|
2280
|
+
item_name=lib_path, # Use path as fallback since we don't have library_name
|
|
2281
|
+
status=InitializationStatus.FAILED,
|
|
1662
2282
|
current=current_library_index,
|
|
1663
2283
|
total=total_libraries,
|
|
2284
|
+
error=error_message,
|
|
1664
2285
|
)
|
|
1665
2286
|
)
|
|
1666
2287
|
)
|
|
2288
|
+
continue
|
|
1667
2289
|
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
current=current_library_index,
|
|
1682
|
-
total=total_libraries,
|
|
1683
|
-
)
|
|
2290
|
+
# Success case - narrow type and get library_name from result
|
|
2291
|
+
if isinstance(load_result, RegisterLibraryFromFileResultSuccess):
|
|
2292
|
+
library_name = load_result.library_name
|
|
2293
|
+
|
|
2294
|
+
# Emit success event
|
|
2295
|
+
GriptapeNodes.EventManager().put_event(
|
|
2296
|
+
AppEvent(
|
|
2297
|
+
payload=EngineInitializationProgress(
|
|
2298
|
+
phase=InitializationPhase.LIBRARIES,
|
|
2299
|
+
item_name=library_name,
|
|
2300
|
+
status=InitializationStatus.COMPLETE,
|
|
2301
|
+
current=current_library_index,
|
|
2302
|
+
total=total_libraries,
|
|
1684
2303
|
)
|
|
1685
2304
|
)
|
|
1686
|
-
|
|
1687
|
-
# Handle config-based library - register it directly using the file path
|
|
1688
|
-
register_request = RegisterLibraryFromFileRequest(
|
|
1689
|
-
file_path=library_result.file_path, load_as_default_library=False
|
|
1690
|
-
)
|
|
1691
|
-
register_result = await self.register_library_from_file_request(register_request)
|
|
1692
|
-
if isinstance(register_result, RegisterLibraryFromFileResultFailure):
|
|
1693
|
-
# Registration failed - the failure info is already recorded in _library_file_path_to_info
|
|
1694
|
-
# by register_library_from_file_request, so we just log it here for visibility
|
|
1695
|
-
logger.warning("Failed to register library from %s", library_result.file_path)
|
|
1696
|
-
# Emit failure event
|
|
1697
|
-
error_message = (
|
|
1698
|
-
register_result.result_details.result_details[0].message
|
|
1699
|
-
if isinstance(register_result.result_details, ResultDetails)
|
|
1700
|
-
else register_result.result_details
|
|
1701
|
-
)
|
|
1702
|
-
GriptapeNodes.EventManager().put_event(
|
|
1703
|
-
AppEvent(
|
|
1704
|
-
payload=EngineInitializationProgress(
|
|
1705
|
-
phase=InitializationPhase.LIBRARIES,
|
|
1706
|
-
item_name=library_name,
|
|
1707
|
-
status=InitializationStatus.FAILED,
|
|
1708
|
-
current=current_library_index,
|
|
1709
|
-
total=total_libraries,
|
|
1710
|
-
error=error_message,
|
|
1711
|
-
)
|
|
1712
|
-
)
|
|
1713
|
-
)
|
|
1714
|
-
else:
|
|
1715
|
-
# Emit success event
|
|
1716
|
-
GriptapeNodes.EventManager().put_event(
|
|
1717
|
-
AppEvent(
|
|
1718
|
-
payload=EngineInitializationProgress(
|
|
1719
|
-
phase=InitializationPhase.LIBRARIES,
|
|
1720
|
-
item_name=library_name,
|
|
1721
|
-
status=InitializationStatus.COMPLETE,
|
|
1722
|
-
current=current_library_index,
|
|
1723
|
-
total=total_libraries,
|
|
1724
|
-
)
|
|
1725
|
-
)
|
|
1726
|
-
)
|
|
2305
|
+
)
|
|
1727
2306
|
|
|
1728
|
-
|
|
1729
|
-
|
|
2307
|
+
# Print 'em all pretty
|
|
2308
|
+
self.print_library_load_status()
|
|
1730
2309
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
2310
|
+
# Remove any missing libraries AFTER we've printed them for the user.
|
|
2311
|
+
user_libraries_section = LIBRARIES_TO_REGISTER_KEY
|
|
2312
|
+
self._remove_missing_libraries_from_config(config_category=user_libraries_section)
|
|
2313
|
+
|
|
2314
|
+
# Mark libraries loading as complete
|
|
2315
|
+
self._libraries_loading_complete.set()
|
|
1736
2316
|
|
|
1737
2317
|
async def _ensure_libraries_from_config(self) -> None:
|
|
1738
2318
|
"""Ensure libraries from git URLs specified in config are downloaded.
|
|
@@ -1923,86 +2503,6 @@ class LibraryManager:
|
|
|
1923
2503
|
)
|
|
1924
2504
|
console.print(message)
|
|
1925
2505
|
|
|
1926
|
-
async def _load_libraries_from_provenance_system(self) -> None:
|
|
1927
|
-
"""Load libraries using the new provenance-based system with FSM.
|
|
1928
|
-
|
|
1929
|
-
This method converts libraries_to_register entries into LibraryProvenanceLocalFile
|
|
1930
|
-
objects and processes them through the LibraryDirectory and LibraryLifecycleFSM systems.
|
|
1931
|
-
"""
|
|
1932
|
-
# Get config manager
|
|
1933
|
-
config_mgr = GriptapeNodes.ConfigManager()
|
|
1934
|
-
|
|
1935
|
-
# Get the current libraries_to_register list
|
|
1936
|
-
user_libraries_section = LIBRARIES_TO_REGISTER_KEY
|
|
1937
|
-
libraries_to_register: list[str] = config_mgr.get_config_value(user_libraries_section)
|
|
1938
|
-
|
|
1939
|
-
# Filter out empty or whitespace-only entries
|
|
1940
|
-
original_count = len(libraries_to_register) if libraries_to_register else 0
|
|
1941
|
-
libraries_to_register = [path for path in (libraries_to_register or []) if path and path.strip()]
|
|
1942
|
-
filtered_count = original_count - len(libraries_to_register)
|
|
1943
|
-
if filtered_count > 0:
|
|
1944
|
-
logger.warning("Filtered out %d empty library path entries from configuration", filtered_count)
|
|
1945
|
-
|
|
1946
|
-
if not libraries_to_register:
|
|
1947
|
-
logger.info("No libraries to register from config")
|
|
1948
|
-
return
|
|
1949
|
-
|
|
1950
|
-
# Convert string paths to LibraryProvenanceLocalFile objects
|
|
1951
|
-
for library_path in libraries_to_register:
|
|
1952
|
-
# Skip non-JSON files for now (requirement specifiers will need different handling)
|
|
1953
|
-
if not library_path.endswith(".json"):
|
|
1954
|
-
logger.debug("Skipping non-JSON library path: %s", library_path)
|
|
1955
|
-
continue
|
|
1956
|
-
|
|
1957
|
-
# Create provenance object
|
|
1958
|
-
provenance = LibraryProvenanceLocalFile(file_path=library_path)
|
|
1959
|
-
|
|
1960
|
-
# Add to directory as user candidate (defaults to active=True)
|
|
1961
|
-
# This automatically creates FSM and runs evaluation
|
|
1962
|
-
await self._library_directory.add_user_candidate(provenance)
|
|
1963
|
-
|
|
1964
|
-
logger.debug("Added library provenance: %s", provenance.get_display_name())
|
|
1965
|
-
|
|
1966
|
-
# Get all candidates for evaluation
|
|
1967
|
-
all_candidates = self._library_directory.get_all_candidates()
|
|
1968
|
-
|
|
1969
|
-
logger.info("Evaluated %d library candidates through FSM lifecycle", len(all_candidates))
|
|
1970
|
-
|
|
1971
|
-
# Report on conflicts found
|
|
1972
|
-
self._report_library_name_conflicts()
|
|
1973
|
-
|
|
1974
|
-
# Get candidates that are ready for installation
|
|
1975
|
-
installable_candidates = self._library_directory.get_installable_candidates()
|
|
1976
|
-
|
|
1977
|
-
# Log any skipped libraries
|
|
1978
|
-
active_candidates = self._library_directory.get_active_candidates()
|
|
1979
|
-
for candidate in active_candidates:
|
|
1980
|
-
if candidate not in installable_candidates:
|
|
1981
|
-
blockers = self._library_directory.get_installation_blockers(candidate.provenance)
|
|
1982
|
-
if blockers:
|
|
1983
|
-
blocker_messages = [blocker.message for blocker in blockers]
|
|
1984
|
-
combined_message = "; ".join(blocker_messages)
|
|
1985
|
-
logger.info("Skipping library '%s' - %s", candidate.provenance.get_display_name(), combined_message)
|
|
1986
|
-
|
|
1987
|
-
logger.info("Installing and loading %d installable library candidates", len(installable_candidates))
|
|
1988
|
-
|
|
1989
|
-
# Process installable candidates through installation and loading
|
|
1990
|
-
for candidate in installable_candidates:
|
|
1991
|
-
if await self._library_directory.install_library(candidate.provenance):
|
|
1992
|
-
await self._library_directory.load_library(candidate.provenance)
|
|
1993
|
-
|
|
1994
|
-
def _report_library_name_conflicts(self) -> None:
|
|
1995
|
-
"""Report on library name conflicts found during evaluation."""
|
|
1996
|
-
conflicting_names = self._library_directory.get_all_conflicting_library_names()
|
|
1997
|
-
for library_name in conflicting_names:
|
|
1998
|
-
conflicting_provenances = self._library_directory.get_conflicting_provenances(library_name)
|
|
1999
|
-
logger.warning(
|
|
2000
|
-
"Library name conflict detected for '%s' across %d libraries: %s",
|
|
2001
|
-
library_name,
|
|
2002
|
-
len(conflicting_provenances),
|
|
2003
|
-
[p.get_display_name() for p in conflicting_provenances],
|
|
2004
|
-
)
|
|
2005
|
-
|
|
2006
2506
|
def _load_advanced_library_module(
|
|
2007
2507
|
self,
|
|
2008
2508
|
library_data: LibrarySchema,
|
|
@@ -2069,54 +2569,42 @@ class LibraryManager:
|
|
|
2069
2569
|
|
|
2070
2570
|
return advanced_library_instance
|
|
2071
2571
|
|
|
2072
|
-
def _attempt_load_nodes_from_library( # noqa:
|
|
2572
|
+
def _attempt_load_nodes_from_library( # noqa: PLR0912, PLR0915, C901
|
|
2073
2573
|
self,
|
|
2074
2574
|
library_data: LibrarySchema,
|
|
2075
2575
|
library: Library,
|
|
2076
2576
|
base_dir: Path,
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
) -> LibraryManager.LibraryInfo:
|
|
2081
|
-
any_nodes_loaded_successfully = False
|
|
2577
|
+
library_info: LibraryInfo,
|
|
2578
|
+
) -> None:
|
|
2579
|
+
"""Load nodes from library and update library_info in place.
|
|
2082
2580
|
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
problems
|
|
2088
|
-
|
|
2089
|
-
|
|
2581
|
+
Args:
|
|
2582
|
+
library_data: Library schema with node definitions
|
|
2583
|
+
library: Library instance to register nodes with
|
|
2584
|
+
base_dir: Base directory for resolving relative paths
|
|
2585
|
+
library_info: LibraryInfo to update with problems and fitness
|
|
2586
|
+
"""
|
|
2587
|
+
any_nodes_loaded_successfully = False
|
|
2090
2588
|
|
|
2091
2589
|
# Check if library is in old XDG location
|
|
2092
2590
|
old_xdg_libraries_path = xdg_data_home() / "griptape_nodes" / "libraries"
|
|
2093
|
-
library_path_obj = Path(
|
|
2591
|
+
library_path_obj = Path(library_info.library_path)
|
|
2094
2592
|
try:
|
|
2095
2593
|
# Check if the library path is relative to the old XDG location
|
|
2096
2594
|
if library_path_obj.is_relative_to(old_xdg_libraries_path):
|
|
2097
|
-
problems.append(OldXdgLocationWarningProblem(old_path=str(library_path_obj)))
|
|
2595
|
+
library_info.problems.append(OldXdgLocationWarningProblem(old_path=str(library_path_obj)))
|
|
2098
2596
|
logger.warning(
|
|
2099
2597
|
"Library '%s' is located in old XDG data directory: %s. "
|
|
2100
2598
|
"Starting with version 0.65.0, libraries are managed in your workspace directory. "
|
|
2101
2599
|
"To migrate: run 'gtn init' (CLI) or go to App Settings and click 'Re-run Setup Wizard' (desktop app).",
|
|
2102
2600
|
library_data.name,
|
|
2103
|
-
|
|
2601
|
+
library_info.library_path,
|
|
2104
2602
|
)
|
|
2105
2603
|
except ValueError:
|
|
2106
2604
|
# is_relative_to() raises ValueError if paths are on different drives
|
|
2107
2605
|
# In this case, library is definitely not in the old XDG location
|
|
2108
2606
|
pass
|
|
2109
2607
|
|
|
2110
|
-
# Early exit if any version issues are disqualifying
|
|
2111
|
-
if has_disqualifying_issues:
|
|
2112
|
-
return LibraryManager.LibraryInfo(
|
|
2113
|
-
library_path=library_file_path,
|
|
2114
|
-
library_name=library_data.name,
|
|
2115
|
-
library_version=library_version,
|
|
2116
|
-
status=LibraryStatus.UNUSABLE,
|
|
2117
|
-
problems=problems,
|
|
2118
|
-
)
|
|
2119
|
-
|
|
2120
2608
|
# Call the before_library_nodes_loaded callback if available
|
|
2121
2609
|
advanced_library = library.get_advanced_library()
|
|
2122
2610
|
if advanced_library:
|
|
@@ -2125,7 +2613,7 @@ class LibraryManager:
|
|
|
2125
2613
|
details = f"Successfully called before_library_nodes_loaded callback for library '{library_data.name}'"
|
|
2126
2614
|
logger.debug(details)
|
|
2127
2615
|
except Exception as err:
|
|
2128
|
-
problems.append(BeforeLibraryCallbackProblem(error_message=str(err)))
|
|
2616
|
+
library_info.problems.append(BeforeLibraryCallbackProblem(error_message=str(err)))
|
|
2129
2617
|
details = (
|
|
2130
2618
|
f"Failed to call before_library_nodes_loaded callback for library '{library_data.name}': {err}"
|
|
2131
2619
|
)
|
|
@@ -2143,7 +2631,7 @@ class LibraryManager:
|
|
|
2143
2631
|
node_class = self._load_class_from_file(node_file_path, node_definition.class_name, library_data.name)
|
|
2144
2632
|
except ImportError as err:
|
|
2145
2633
|
root_cause = self._get_root_cause_from_exception(err)
|
|
2146
|
-
problems.append(
|
|
2634
|
+
library_info.problems.append(
|
|
2147
2635
|
NodeModuleImportProblem(
|
|
2148
2636
|
class_name=node_definition.class_name,
|
|
2149
2637
|
file_path=str(node_file_path),
|
|
@@ -2155,14 +2643,14 @@ class LibraryManager:
|
|
|
2155
2643
|
logger.error(details)
|
|
2156
2644
|
continue # SKIP IT
|
|
2157
2645
|
except AttributeError:
|
|
2158
|
-
problems.append(
|
|
2646
|
+
library_info.problems.append(
|
|
2159
2647
|
NodeClassNotFoundProblem(class_name=node_definition.class_name, file_path=str(node_file_path))
|
|
2160
2648
|
)
|
|
2161
2649
|
details = f"Attempted to load node '{node_definition.class_name}' from '{node_file_path}'. Failed because class not found in module"
|
|
2162
2650
|
logger.error(details)
|
|
2163
2651
|
continue # SKIP IT
|
|
2164
2652
|
except TypeError:
|
|
2165
|
-
problems.append(
|
|
2653
|
+
library_info.problems.append(
|
|
2166
2654
|
NodeClassNotBaseNodeProblem(class_name=node_definition.class_name, file_path=str(node_file_path))
|
|
2167
2655
|
)
|
|
2168
2656
|
details = f"Attempted to load node '{node_definition.class_name}' from '{node_file_path}'. Failed because class doesn't inherit from BaseNode"
|
|
@@ -2172,7 +2660,7 @@ class LibraryManager:
|
|
|
2172
2660
|
# Register the node type with the library
|
|
2173
2661
|
library_problem = library.register_new_node_type(node_class, metadata=node_definition.metadata)
|
|
2174
2662
|
if library_problem is not None:
|
|
2175
|
-
problems.append(library_problem)
|
|
2663
|
+
library_info.problems.append(library_problem)
|
|
2176
2664
|
|
|
2177
2665
|
# If we got here, at least one node came in.
|
|
2178
2666
|
any_nodes_loaded_successfully = True
|
|
@@ -2184,49 +2672,46 @@ class LibraryManager:
|
|
|
2184
2672
|
details = f"Successfully called after_library_nodes_loaded callback for library '{library_data.name}'"
|
|
2185
2673
|
logger.debug(details)
|
|
2186
2674
|
except Exception as err:
|
|
2187
|
-
problems.append(AfterLibraryCallbackProblem(error_message=str(err)))
|
|
2675
|
+
library_info.problems.append(AfterLibraryCallbackProblem(error_message=str(err)))
|
|
2188
2676
|
details = f"Failed to call after_library_nodes_loaded callback for library '{library_data.name}': {err}"
|
|
2189
2677
|
logger.error(details)
|
|
2190
2678
|
|
|
2191
|
-
#
|
|
2679
|
+
# Update library_info fitness based on load successes and problem count
|
|
2192
2680
|
if not any_nodes_loaded_successfully:
|
|
2193
|
-
|
|
2194
|
-
elif problems:
|
|
2681
|
+
library_info.fitness = LibraryManager.LibraryFitness.UNUSABLE
|
|
2682
|
+
elif library_info.problems:
|
|
2195
2683
|
# Success, but errors.
|
|
2196
|
-
|
|
2684
|
+
library_info.fitness = LibraryManager.LibraryFitness.FLAWED
|
|
2197
2685
|
else:
|
|
2198
2686
|
# Flawless victory.
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
# Create a LibraryInfo object based on load successes and problem count.
|
|
2202
|
-
return LibraryManager.LibraryInfo(
|
|
2203
|
-
library_path=library_file_path,
|
|
2204
|
-
library_name=library_data.name,
|
|
2205
|
-
library_version=library_version,
|
|
2206
|
-
status=status,
|
|
2207
|
-
problems=problems,
|
|
2208
|
-
)
|
|
2687
|
+
library_info.fitness = LibraryManager.LibraryFitness.GOOD
|
|
2209
2688
|
|
|
2210
|
-
|
|
2211
|
-
|
|
2689
|
+
# Update lifecycle state to LOADED
|
|
2690
|
+
library_info.lifecycle_state = LibraryManager.LibraryLifecycleState.LOADED
|
|
2691
|
+
|
|
2692
|
+
async def _attempt_generate_sandbox_library_from_schema( # noqa: C901
|
|
2693
|
+
self,
|
|
2694
|
+
library_schema: LibrarySchema,
|
|
2695
|
+
sandbox_directory: str,
|
|
2696
|
+
library_info: LibraryInfo,
|
|
2212
2697
|
) -> None:
|
|
2213
2698
|
"""Generate sandbox library using an existing schema, loading actual node classes."""
|
|
2214
2699
|
sandbox_library_dir = Path(sandbox_directory)
|
|
2215
|
-
sandbox_library_dir_as_posix = sandbox_library_dir.as_posix()
|
|
2216
2700
|
|
|
2217
2701
|
problems = []
|
|
2218
2702
|
|
|
2219
2703
|
# Get the file paths from the schema's node definitions to load actual classes
|
|
2220
2704
|
actual_node_definitions = []
|
|
2221
2705
|
for node_def in library_schema.nodes:
|
|
2222
|
-
|
|
2706
|
+
# Resolve relative path from schema against sandbox directory
|
|
2707
|
+
candidate_path = sandbox_library_dir / node_def.file_path
|
|
2223
2708
|
try:
|
|
2224
2709
|
module = self._load_module_from_file(candidate_path, LibraryManager.SANDBOX_LIBRARY_NAME)
|
|
2225
2710
|
except Exception as err:
|
|
2226
2711
|
root_cause = self._get_root_cause_from_exception(err)
|
|
2227
2712
|
problems.append(
|
|
2228
2713
|
NodeModuleImportProblem(
|
|
2229
|
-
class_name=node_def.
|
|
2714
|
+
class_name=f"<Sandbox node in '{node_def.file_path}'>",
|
|
2230
2715
|
file_path=str(candidate_path),
|
|
2231
2716
|
error_message=str(err),
|
|
2232
2717
|
root_cause=str(root_cause),
|
|
@@ -2247,29 +2732,38 @@ class LibraryManager:
|
|
|
2247
2732
|
details = f"Found node '{class_name}' in sandbox library '{candidate_path}'."
|
|
2248
2733
|
logger.debug(details)
|
|
2249
2734
|
|
|
2250
|
-
#
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2735
|
+
# Look for existing node definition to preserve user-edited metadata
|
|
2736
|
+
existing_node = None
|
|
2737
|
+
for existing_node_def in library_schema.nodes:
|
|
2738
|
+
if (
|
|
2739
|
+
existing_node_def.file_path == str(candidate_path)
|
|
2740
|
+
and existing_node_def.class_name == class_name
|
|
2741
|
+
):
|
|
2742
|
+
existing_node = existing_node_def
|
|
2743
|
+
break
|
|
2744
|
+
|
|
2745
|
+
if existing_node:
|
|
2746
|
+
# PRESERVE existing metadata - user may have customized it
|
|
2747
|
+
node_metadata = existing_node.metadata
|
|
2748
|
+
logger.debug("Preserving existing metadata for node '%s'", class_name)
|
|
2749
|
+
else:
|
|
2750
|
+
# NEW node - create default metadata
|
|
2751
|
+
node_metadata = NodeMetadata(
|
|
2752
|
+
category=self.SANDBOX_CATEGORY_NAME,
|
|
2753
|
+
description=f"'{class_name}' (loaded from the {LibraryManager.SANDBOX_LIBRARY_NAME}).",
|
|
2754
|
+
display_name=class_name,
|
|
2755
|
+
)
|
|
2756
|
+
logger.debug("Creating new metadata for node '%s'", class_name)
|
|
2757
|
+
|
|
2264
2758
|
node_definition = NodeDefinition(
|
|
2265
2759
|
class_name=class_name,
|
|
2266
|
-
file_path=
|
|
2760
|
+
file_path=node_def.file_path, # Keep original relative path from schema
|
|
2267
2761
|
metadata=node_metadata,
|
|
2268
2762
|
)
|
|
2269
2763
|
actual_node_definitions.append(node_definition)
|
|
2270
2764
|
|
|
2271
2765
|
if not actual_node_definitions:
|
|
2272
|
-
logger.
|
|
2766
|
+
logger.debug("No nodes found in sandbox library '%s'. Skipping.", sandbox_library_dir)
|
|
2273
2767
|
return
|
|
2274
2768
|
|
|
2275
2769
|
# Use the existing schema but replace nodes with actual discovered ones
|
|
@@ -2281,6 +2775,16 @@ class LibraryManager:
|
|
|
2281
2775
|
nodes=actual_node_definitions,
|
|
2282
2776
|
)
|
|
2283
2777
|
|
|
2778
|
+
# Save the schema with real class names back to disk
|
|
2779
|
+
json_path = sandbox_library_dir / LibraryManager.LIBRARY_CONFIG_FILENAME
|
|
2780
|
+
write_succeeded = self._write_library_schema_to_json(library_data, json_path)
|
|
2781
|
+
if write_succeeded:
|
|
2782
|
+
logger.debug(
|
|
2783
|
+
"Saved sandbox library schema with %d discovered nodes to '%s'",
|
|
2784
|
+
len(actual_node_definitions),
|
|
2785
|
+
json_path,
|
|
2786
|
+
)
|
|
2787
|
+
|
|
2284
2788
|
# Register the library.
|
|
2285
2789
|
# Create or get the library
|
|
2286
2790
|
try:
|
|
@@ -2291,30 +2795,27 @@ class LibraryManager:
|
|
|
2291
2795
|
)
|
|
2292
2796
|
|
|
2293
2797
|
except KeyError as err:
|
|
2294
|
-
# Library already exists
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
library_version=library_data.metadata.library_version,
|
|
2299
|
-
status=LibraryStatus.UNUSABLE,
|
|
2300
|
-
problems=[DuplicateLibraryProblem()],
|
|
2301
|
-
)
|
|
2798
|
+
# Library already exists - update existing library_info
|
|
2799
|
+
library_info.lifecycle_state = LibraryManager.LibraryLifecycleState.FAILURE
|
|
2800
|
+
library_info.fitness = LibraryManager.LibraryFitness.UNUSABLE
|
|
2801
|
+
library_info.problems.append(DuplicateLibraryProblem())
|
|
2302
2802
|
|
|
2303
2803
|
details = f"Attempted to load Library JSON file from '{sandbox_library_dir}'. Failed because a Library '{library_data.name}' already exists. Error: {err}."
|
|
2304
2804
|
logger.error(details)
|
|
2305
2805
|
return
|
|
2306
2806
|
|
|
2307
|
-
#
|
|
2308
|
-
|
|
2807
|
+
# Add any problems encountered during node discovery to library_info
|
|
2808
|
+
library_info.problems.extend(problems)
|
|
2809
|
+
|
|
2810
|
+
# Load nodes into the library (modifies library_info in place)
|
|
2811
|
+
# Note: library_info is passed as parameter from lifecycle handler
|
|
2812
|
+
await asyncio.to_thread(
|
|
2309
2813
|
self._attempt_load_nodes_from_library,
|
|
2310
2814
|
library_data=library_data,
|
|
2311
2815
|
library=library,
|
|
2312
2816
|
base_dir=sandbox_library_dir,
|
|
2313
|
-
|
|
2314
|
-
library_version=library_data.metadata.library_version,
|
|
2315
|
-
problems=problems,
|
|
2817
|
+
library_info=library_info,
|
|
2316
2818
|
)
|
|
2317
|
-
self._library_file_path_to_info[sandbox_library_dir_as_posix] = library_load_results
|
|
2318
2819
|
|
|
2319
2820
|
def _find_files_in_dir(self, directory: Path, extension: str) -> list[Path]:
|
|
2320
2821
|
"""Find all files with given extension in directory, excluding common non-source directories."""
|
|
@@ -2330,6 +2831,29 @@ class LibraryManager:
|
|
|
2330
2831
|
ret_val.append(file_path)
|
|
2331
2832
|
return ret_val
|
|
2332
2833
|
|
|
2834
|
+
def _write_library_schema_to_json(self, library_schema: LibrarySchema, json_path: Path) -> bool:
|
|
2835
|
+
"""Write library schema to JSON file using WriteFileRequest.
|
|
2836
|
+
|
|
2837
|
+
Args:
|
|
2838
|
+
library_schema: The library schema to write
|
|
2839
|
+
json_path: Path where the JSON file should be written
|
|
2840
|
+
|
|
2841
|
+
Returns:
|
|
2842
|
+
True if write succeeded, False otherwise
|
|
2843
|
+
"""
|
|
2844
|
+
write_request = WriteFileRequest(
|
|
2845
|
+
file_path=str(json_path),
|
|
2846
|
+
content=library_schema.model_dump_json(indent=2),
|
|
2847
|
+
encoding="utf-8",
|
|
2848
|
+
)
|
|
2849
|
+
write_result = GriptapeNodes.handle_request(write_request)
|
|
2850
|
+
|
|
2851
|
+
if write_result.failed():
|
|
2852
|
+
logger.error("Failed to write library schema to '%s': %s", json_path, write_result.result_details)
|
|
2853
|
+
return False
|
|
2854
|
+
|
|
2855
|
+
return True
|
|
2856
|
+
|
|
2333
2857
|
def _remove_missing_libraries_from_config(self, config_category: str) -> None:
|
|
2334
2858
|
# Now remove all libraries that were missing from the user's config.
|
|
2335
2859
|
config_mgr = GriptapeNodes.ConfigManager()
|
|
@@ -2337,7 +2861,7 @@ class LibraryManager:
|
|
|
2337
2861
|
|
|
2338
2862
|
paths_to_remove = set()
|
|
2339
2863
|
for library_path, library_info in self._library_file_path_to_info.items():
|
|
2340
|
-
if library_info.
|
|
2864
|
+
if library_info.fitness == LibraryManager.LibraryFitness.MISSING:
|
|
2341
2865
|
# Remove this file path from the config.
|
|
2342
2866
|
paths_to_remove.add(library_path.lower())
|
|
2343
2867
|
|
|
@@ -2461,24 +2985,236 @@ class LibraryManager:
|
|
|
2461
2985
|
)
|
|
2462
2986
|
return ReloadAllLibrariesResultSuccess(result_details=ResultDetails(message=details, level=logging.INFO))
|
|
2463
2987
|
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
if not unloaded_libraries:
|
|
2471
|
-
details = "All configured libraries are already loaded, no action needed."
|
|
2472
|
-
return LoadLibrariesResultSuccess(result_details=ResultDetails(message=details, level=logging.INFO))
|
|
2988
|
+
def discover_libraries_request(
|
|
2989
|
+
self,
|
|
2990
|
+
request: DiscoverLibrariesRequest,
|
|
2991
|
+
) -> DiscoverLibrariesResultSuccess | DiscoverLibrariesResultFailure:
|
|
2992
|
+
"""Discover libraries from config and track them in discovered state.
|
|
2473
2993
|
|
|
2994
|
+
This is the event handler for DiscoverLibrariesRequest.
|
|
2995
|
+
Scans configured library paths and creates LibraryInfo entries in DISCOVERED state.
|
|
2996
|
+
"""
|
|
2474
2997
|
try:
|
|
2475
|
-
|
|
2476
|
-
details = "Successfully loaded all libraries from configuration."
|
|
2477
|
-
return LoadLibrariesResultSuccess(result_details=ResultDetails(message=details, level=logging.INFO))
|
|
2998
|
+
config_library_paths = set(self._discover_library_files())
|
|
2478
2999
|
except Exception as e:
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
3000
|
+
logger.exception("Failed to discover library files")
|
|
3001
|
+
return DiscoverLibrariesResultFailure(
|
|
3002
|
+
result_details=f"Failed to discover library files: {e}",
|
|
3003
|
+
)
|
|
3004
|
+
|
|
3005
|
+
discovered_libraries = set()
|
|
3006
|
+
|
|
3007
|
+
# Process sandbox library first if requested
|
|
3008
|
+
if request.include_sandbox:
|
|
3009
|
+
sandbox_library_dir = self._get_sandbox_directory()
|
|
3010
|
+
if sandbox_library_dir:
|
|
3011
|
+
# Generate/update the sandbox library JSON file
|
|
3012
|
+
metadata_result = self.scan_sandbox_directory_request(
|
|
3013
|
+
ScanSandboxDirectoryRequest(directory_path=str(sandbox_library_dir))
|
|
3014
|
+
)
|
|
3015
|
+
|
|
3016
|
+
# If generation succeeded, write JSON and add the sandbox library
|
|
3017
|
+
if isinstance(metadata_result, ScanSandboxDirectoryResultSuccess):
|
|
3018
|
+
sandbox_json_path = sandbox_library_dir / LibraryManager.LIBRARY_CONFIG_FILENAME
|
|
3019
|
+
sandbox_json_path_str = str(sandbox_json_path)
|
|
3020
|
+
|
|
3021
|
+
# Write the schema to JSON so it exists for lifecycle phases
|
|
3022
|
+
write_succeeded = self._write_library_schema_to_json(
|
|
3023
|
+
metadata_result.library_schema, sandbox_json_path
|
|
3024
|
+
)
|
|
3025
|
+
if write_succeeded:
|
|
3026
|
+
logger.debug(
|
|
3027
|
+
"Wrote sandbox library schema with %d nodes to '%s' during discovery",
|
|
3028
|
+
len(metadata_result.library_schema.nodes),
|
|
3029
|
+
sandbox_json_path,
|
|
3030
|
+
)
|
|
3031
|
+
# Continue anyway if write failed - lifecycle will fail gracefully
|
|
3032
|
+
|
|
3033
|
+
# Add to discovered libraries with is_sandbox=True
|
|
3034
|
+
discovered_libraries.add(DiscoveredLibrary(path=sandbox_json_path, is_sandbox=True))
|
|
3035
|
+
|
|
3036
|
+
# Create minimal LibraryInfo entry in discovered state if not already tracked
|
|
3037
|
+
if sandbox_json_path_str not in self._library_file_path_to_info:
|
|
3038
|
+
self._library_file_path_to_info[sandbox_json_path_str] = LibraryManager.LibraryInfo(
|
|
3039
|
+
lifecycle_state=LibraryManager.LibraryLifecycleState.DISCOVERED,
|
|
3040
|
+
fitness=LibraryManager.LibraryFitness.NOT_EVALUATED,
|
|
3041
|
+
library_path=sandbox_json_path_str,
|
|
3042
|
+
is_sandbox=True,
|
|
3043
|
+
library_name=None,
|
|
3044
|
+
library_version=None,
|
|
3045
|
+
)
|
|
3046
|
+
|
|
3047
|
+
# Add all regular libraries from config
|
|
3048
|
+
for file_path in config_library_paths:
|
|
3049
|
+
file_path_str = str(file_path)
|
|
3050
|
+
|
|
3051
|
+
# Add to discovered libraries with is_sandbox=False
|
|
3052
|
+
discovered_libraries.add(DiscoveredLibrary(path=file_path, is_sandbox=False))
|
|
3053
|
+
|
|
3054
|
+
# Skip if already tracked
|
|
3055
|
+
if file_path_str in self._library_file_path_to_info:
|
|
3056
|
+
continue
|
|
3057
|
+
|
|
3058
|
+
# Create minimal LibraryInfo entry in discovered state
|
|
3059
|
+
self._library_file_path_to_info[file_path_str] = LibraryManager.LibraryInfo(
|
|
3060
|
+
lifecycle_state=LibraryManager.LibraryLifecycleState.DISCOVERED,
|
|
3061
|
+
fitness=LibraryManager.LibraryFitness.NOT_EVALUATED,
|
|
3062
|
+
library_path=file_path_str,
|
|
3063
|
+
is_sandbox=False,
|
|
3064
|
+
library_name=None,
|
|
3065
|
+
library_version=None,
|
|
3066
|
+
)
|
|
3067
|
+
|
|
3068
|
+
# Success path at the end
|
|
3069
|
+
return DiscoverLibrariesResultSuccess(
|
|
3070
|
+
result_details=f"Discovered {len(discovered_libraries)} libraries",
|
|
3071
|
+
libraries_discovered=discovered_libraries,
|
|
3072
|
+
)
|
|
3073
|
+
|
|
3074
|
+
def evaluate_library_fitness_request(
|
|
3075
|
+
self, request: EvaluateLibraryFitnessRequest
|
|
3076
|
+
) -> EvaluateLibraryFitnessResultSuccess | EvaluateLibraryFitnessResultFailure:
|
|
3077
|
+
"""Evaluate library fitness using version compatibility checks.
|
|
3078
|
+
|
|
3079
|
+
Extracts version checking logic from _attempt_load_nodes_from_library.
|
|
3080
|
+
Checks engine version compatibility without loading Python modules.
|
|
3081
|
+
"""
|
|
3082
|
+
schema = request.schema
|
|
3083
|
+
problems: list[LibraryProblem] = []
|
|
3084
|
+
|
|
3085
|
+
# Check for version-based compatibility issues
|
|
3086
|
+
version_issues = GriptapeNodes.VersionCompatibilityManager().check_library_version_compatibility(schema)
|
|
3087
|
+
has_disqualifying_issues = False
|
|
3088
|
+
|
|
3089
|
+
for issue in version_issues:
|
|
3090
|
+
problems.append(issue.problem)
|
|
3091
|
+
if issue.severity == LibraryManager.LibraryFitness.UNUSABLE:
|
|
3092
|
+
has_disqualifying_issues = True
|
|
3093
|
+
|
|
3094
|
+
if has_disqualifying_issues:
|
|
3095
|
+
return EvaluateLibraryFitnessResultFailure(
|
|
3096
|
+
result_details=f"Library '{schema.name}' has version compatibility issues",
|
|
3097
|
+
fitness=LibraryManager.LibraryFitness.UNUSABLE,
|
|
3098
|
+
problems=problems,
|
|
3099
|
+
)
|
|
3100
|
+
|
|
3101
|
+
# Determine fitness based on whether we have any non-disqualifying issues
|
|
3102
|
+
fitness = LibraryManager.LibraryFitness.FLAWED if problems else LibraryManager.LibraryFitness.GOOD
|
|
3103
|
+
|
|
3104
|
+
return EvaluateLibraryFitnessResultSuccess(
|
|
3105
|
+
result_details=f"Library '{schema.name}' is compatible",
|
|
3106
|
+
fitness=fitness,
|
|
3107
|
+
problems=problems,
|
|
3108
|
+
)
|
|
3109
|
+
|
|
3110
|
+
async def load_libraries_request(self, request: LoadLibrariesRequest) -> ResultPayload: # noqa: ARG002, C901
|
|
3111
|
+
"""Load all libraries from configuration (backward compatibility wrapper).
|
|
3112
|
+
|
|
3113
|
+
This is the legacy entry point that loads all configured libraries.
|
|
3114
|
+
New code should use LoadLibraryRequest to load specific libraries instead.
|
|
3115
|
+
"""
|
|
3116
|
+
# First, discover all available libraries
|
|
3117
|
+
discover_result = self.discover_libraries_request(DiscoverLibrariesRequest())
|
|
3118
|
+
if isinstance(discover_result, DiscoverLibrariesResultFailure):
|
|
3119
|
+
return LoadLibrariesResultFailure(result_details=f"Discovery failed: {discover_result.result_details}")
|
|
3120
|
+
|
|
3121
|
+
# Build list of library paths to load, preserving is_sandbox flag
|
|
3122
|
+
libraries_to_load = []
|
|
3123
|
+
for discovered_lib in discover_result.libraries_discovered:
|
|
3124
|
+
lib_path = str(discovered_lib.path)
|
|
3125
|
+
lib_info = self._library_file_path_to_info.get(lib_path)
|
|
3126
|
+
|
|
3127
|
+
# Update is_sandbox if library_info exists and discovery says it's sandbox
|
|
3128
|
+
if lib_info and discovered_lib.is_sandbox:
|
|
3129
|
+
lib_info.is_sandbox = True
|
|
3130
|
+
|
|
3131
|
+
if lib_info:
|
|
3132
|
+
libraries_to_load.append(lib_path)
|
|
3133
|
+
|
|
3134
|
+
if not libraries_to_load:
|
|
3135
|
+
details = "No libraries found in configuration."
|
|
3136
|
+
return LoadLibrariesResultSuccess(result_details=ResultDetails(message=details, level=logging.INFO))
|
|
3137
|
+
|
|
3138
|
+
# Load each discovered library by path
|
|
3139
|
+
loaded_count = 0
|
|
3140
|
+
failed_libraries = []
|
|
3141
|
+
total_libraries = len(libraries_to_load)
|
|
3142
|
+
|
|
3143
|
+
for current_library_index, lib_path in enumerate(libraries_to_load, start=1):
|
|
3144
|
+
load_result = await self.register_library_from_file_request(
|
|
3145
|
+
RegisterLibraryFromFileRequest(
|
|
3146
|
+
file_path=lib_path,
|
|
3147
|
+
load_as_default_library=False,
|
|
3148
|
+
)
|
|
3149
|
+
)
|
|
3150
|
+
|
|
3151
|
+
# Get library_name from result for progress events (use path as fallback for failures)
|
|
3152
|
+
if isinstance(load_result, RegisterLibraryFromFileResultSuccess):
|
|
3153
|
+
library_name = load_result.library_name
|
|
3154
|
+
else:
|
|
3155
|
+
library_name = lib_path
|
|
3156
|
+
|
|
3157
|
+
# Emit loading event
|
|
3158
|
+
GriptapeNodes.EventManager().put_event(
|
|
3159
|
+
AppEvent(
|
|
3160
|
+
payload=EngineInitializationProgress(
|
|
3161
|
+
phase=InitializationPhase.LIBRARIES,
|
|
3162
|
+
item_name=library_name,
|
|
3163
|
+
status=InitializationStatus.LOADING,
|
|
3164
|
+
current=current_library_index,
|
|
3165
|
+
total=total_libraries,
|
|
3166
|
+
)
|
|
3167
|
+
)
|
|
3168
|
+
)
|
|
3169
|
+
|
|
3170
|
+
if isinstance(load_result, RegisterLibraryFromFileResultSuccess):
|
|
3171
|
+
loaded_count += 1
|
|
3172
|
+
|
|
3173
|
+
# Emit success event
|
|
3174
|
+
GriptapeNodes.EventManager().put_event(
|
|
3175
|
+
AppEvent(
|
|
3176
|
+
payload=EngineInitializationProgress(
|
|
3177
|
+
phase=InitializationPhase.LIBRARIES,
|
|
3178
|
+
item_name=library_name,
|
|
3179
|
+
status=InitializationStatus.COMPLETE,
|
|
3180
|
+
current=current_library_index,
|
|
3181
|
+
total=total_libraries,
|
|
3182
|
+
)
|
|
3183
|
+
)
|
|
3184
|
+
)
|
|
3185
|
+
else:
|
|
3186
|
+
failed_libraries.append(library_name)
|
|
3187
|
+
logger.warning("Failed to load library '%s': %s", library_name, load_result.result_details)
|
|
3188
|
+
|
|
3189
|
+
# Emit failure event
|
|
3190
|
+
error_message = (
|
|
3191
|
+
load_result.result_details.result_details[0].message
|
|
3192
|
+
if isinstance(load_result.result_details, ResultDetails)
|
|
3193
|
+
else str(load_result.result_details)
|
|
3194
|
+
)
|
|
3195
|
+
GriptapeNodes.EventManager().put_event(
|
|
3196
|
+
AppEvent(
|
|
3197
|
+
payload=EngineInitializationProgress(
|
|
3198
|
+
phase=InitializationPhase.LIBRARIES,
|
|
3199
|
+
item_name=library_name,
|
|
3200
|
+
status=InitializationStatus.FAILED,
|
|
3201
|
+
current=current_library_index,
|
|
3202
|
+
total=total_libraries,
|
|
3203
|
+
error=error_message,
|
|
3204
|
+
)
|
|
3205
|
+
)
|
|
3206
|
+
)
|
|
3207
|
+
|
|
3208
|
+
if loaded_count == 0 and len(failed_libraries) > 0:
|
|
3209
|
+
return LoadLibrariesResultFailure(
|
|
3210
|
+
result_details=f"Failed to load any libraries. Failed: {', '.join(failed_libraries)}"
|
|
3211
|
+
)
|
|
3212
|
+
|
|
3213
|
+
message = f"Loaded {loaded_count} libraries"
|
|
3214
|
+
if failed_libraries:
|
|
3215
|
+
message += f". Failed: {', '.join(failed_libraries)}"
|
|
3216
|
+
|
|
3217
|
+
return LoadLibrariesResultSuccess(result_details=ResultDetails(message=message, level=logging.INFO))
|
|
2482
3218
|
|
|
2483
3219
|
def _discover_library_files(self) -> list[Path]:
|
|
2484
3220
|
"""Discover library JSON files from config and workspace recursively.
|
|
@@ -2720,6 +3456,18 @@ class LibraryManager:
|
|
|
2720
3456
|
# Use the found file path for reloading
|
|
2721
3457
|
actual_library_file_path = str(actual_library_file)
|
|
2722
3458
|
|
|
3459
|
+
# Create LibraryInfo for tracking this library reload
|
|
3460
|
+
lib_info = LibraryManager.LibraryInfo(
|
|
3461
|
+
lifecycle_state=LibraryManager.LibraryLifecycleState.DISCOVERED,
|
|
3462
|
+
library_path=actual_library_file_path,
|
|
3463
|
+
is_sandbox=False,
|
|
3464
|
+
library_name=library_name,
|
|
3465
|
+
fitness=LibraryManager.LibraryFitness.NOT_EVALUATED,
|
|
3466
|
+
problems=[],
|
|
3467
|
+
)
|
|
3468
|
+
# Store lib_info in dict so register handler can find it
|
|
3469
|
+
self._library_file_path_to_info[actual_library_file_path] = lib_info
|
|
3470
|
+
|
|
2723
3471
|
# Reload the library from file
|
|
2724
3472
|
reload_result = await GriptapeNodes.ahandle_request(
|
|
2725
3473
|
RegisterLibraryFromFileRequest(file_path=actual_library_file_path)
|
|
@@ -2954,6 +3702,18 @@ class LibraryManager:
|
|
|
2954
3702
|
|
|
2955
3703
|
# Automatically register the downloaded library (unless disabled for startup downloads)
|
|
2956
3704
|
if request.auto_register:
|
|
3705
|
+
# Create LibraryInfo for tracking this downloaded library
|
|
3706
|
+
lib_info = LibraryManager.LibraryInfo(
|
|
3707
|
+
lifecycle_state=LibraryManager.LibraryLifecycleState.DISCOVERED,
|
|
3708
|
+
library_path=str(library_json_path),
|
|
3709
|
+
is_sandbox=False,
|
|
3710
|
+
library_name=library_name,
|
|
3711
|
+
fitness=LibraryManager.LibraryFitness.NOT_EVALUATED,
|
|
3712
|
+
problems=[],
|
|
3713
|
+
)
|
|
3714
|
+
# Store lib_info in dict so register handler can find it
|
|
3715
|
+
self._library_file_path_to_info[str(library_json_path)] = lib_info
|
|
3716
|
+
|
|
2957
3717
|
register_request = RegisterLibraryFromFileRequest(file_path=str(library_json_path))
|
|
2958
3718
|
register_result = await GriptapeNodes.ahandle_request(register_request)
|
|
2959
3719
|
if not register_result.succeeded():
|