griptape-nodes 0.38.1__py3-none-any.whl → 0.40.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- griptape_nodes/__init__.py +13 -9
- griptape_nodes/app/__init__.py +10 -1
- griptape_nodes/app/app.py +2 -3
- griptape_nodes/app/app_sessions.py +458 -0
- griptape_nodes/bootstrap/workflow_executors/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +213 -0
- griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +13 -0
- griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +1 -1
- griptape_nodes/drivers/storage/__init__.py +4 -0
- griptape_nodes/drivers/storage/storage_backend.py +10 -0
- griptape_nodes/exe_types/core_types.py +5 -1
- griptape_nodes/exe_types/node_types.py +20 -24
- griptape_nodes/machines/node_resolution.py +5 -1
- griptape_nodes/node_library/advanced_node_library.py +51 -0
- griptape_nodes/node_library/library_registry.py +28 -2
- griptape_nodes/node_library/workflow_registry.py +1 -1
- griptape_nodes/retained_mode/events/agent_events.py +15 -2
- griptape_nodes/retained_mode/events/app_events.py +113 -2
- griptape_nodes/retained_mode/events/base_events.py +28 -1
- griptape_nodes/retained_mode/events/library_events.py +111 -1
- griptape_nodes/retained_mode/events/workflow_events.py +1 -0
- griptape_nodes/retained_mode/griptape_nodes.py +240 -18
- griptape_nodes/retained_mode/managers/agent_manager.py +123 -17
- griptape_nodes/retained_mode/managers/flow_manager.py +16 -48
- griptape_nodes/retained_mode/managers/library_manager.py +642 -121
- griptape_nodes/retained_mode/managers/node_manager.py +1 -1
- griptape_nodes/retained_mode/managers/static_files_manager.py +4 -3
- griptape_nodes/retained_mode/managers/workflow_manager.py +666 -37
- griptape_nodes/retained_mode/utils/__init__.py +1 -0
- griptape_nodes/retained_mode/utils/engine_identity.py +131 -0
- griptape_nodes/retained_mode/utils/name_generator.py +162 -0
- griptape_nodes/retained_mode/utils/session_persistence.py +105 -0
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.40.0.dist-info}/METADATA +1 -1
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.40.0.dist-info}/RECORD +37 -27
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.40.0.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.40.0.dist-info}/entry_points.txt +0 -0
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.40.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,7 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
from typing import TYPE_CHECKING, cast
|
|
16
16
|
|
|
17
17
|
import uv
|
|
18
|
-
from packaging.requirements import Requirement
|
|
18
|
+
from packaging.requirements import InvalidRequirement, Requirement
|
|
19
19
|
from pydantic import ValidationError
|
|
20
20
|
from rich.box import HEAVY_EDGE
|
|
21
21
|
from rich.console import Console
|
|
@@ -66,6 +66,11 @@ from griptape_nodes.retained_mode.events.library_events import (
|
|
|
66
66
|
ListNodeTypesInLibraryResultSuccess,
|
|
67
67
|
ListRegisteredLibrariesRequest,
|
|
68
68
|
ListRegisteredLibrariesResultSuccess,
|
|
69
|
+
LoadLibraryMetadataFromFileRequest,
|
|
70
|
+
LoadLibraryMetadataFromFileResultFailure,
|
|
71
|
+
LoadLibraryMetadataFromFileResultSuccess,
|
|
72
|
+
LoadMetadataForAllLibrariesRequest,
|
|
73
|
+
LoadMetadataForAllLibrariesResultSuccess,
|
|
69
74
|
RegisterLibraryFromFileRequest,
|
|
70
75
|
RegisterLibraryFromFileResultFailure,
|
|
71
76
|
RegisterLibraryFromFileResultSuccess,
|
|
@@ -86,6 +91,7 @@ from griptape_nodes.retained_mode.managers.os_manager import OSManager
|
|
|
86
91
|
if TYPE_CHECKING:
|
|
87
92
|
from types import ModuleType
|
|
88
93
|
|
|
94
|
+
from griptape_nodes.node_library.advanced_node_library import AdvancedNodeLibrary
|
|
89
95
|
from griptape_nodes.retained_mode.events.base_events import ResultPayload
|
|
90
96
|
from griptape_nodes.retained_mode.managers.event_manager import EventManager
|
|
91
97
|
|
|
@@ -93,6 +99,8 @@ logger = logging.getLogger("griptape_nodes")
|
|
|
93
99
|
|
|
94
100
|
|
|
95
101
|
class LibraryManager:
|
|
102
|
+
SANDBOX_LIBRARY_NAME = "Sandbox Library"
|
|
103
|
+
|
|
96
104
|
class LibraryStatus(StrEnum):
|
|
97
105
|
"""Status of the library that was attempted to be loaded."""
|
|
98
106
|
|
|
@@ -116,8 +124,30 @@ class LibraryManager:
|
|
|
116
124
|
|
|
117
125
|
_library_file_path_to_info: dict[str, LibraryInfo]
|
|
118
126
|
|
|
127
|
+
# Stable module namespace mappings for workflow serialization
|
|
128
|
+
# These mappings ensure that dynamically loaded modules can be reliably imported
|
|
129
|
+
# in generated workflow code by providing stable, predictable import paths.
|
|
130
|
+
#
|
|
131
|
+
# Example mappings:
|
|
132
|
+
# dynamic to stable module mapping:
|
|
133
|
+
# "gtn_dynamic_module_image_to_video_py_123456789": "griptape_nodes.node_libraries.runwayml_library.image_to_video"
|
|
134
|
+
#
|
|
135
|
+
# stable to dynamic module mapping:
|
|
136
|
+
# "griptape_nodes.node_libraries.runwayml_library.image_to_video": "gtn_dynamic_module_image_to_video_py_123456789"
|
|
137
|
+
#
|
|
138
|
+
# library to stable modules:
|
|
139
|
+
# "RunwayML Library": {"griptape_nodes.node_libraries.runwayml_library.image_to_video", "griptape_nodes.node_libraries.runwayml_library.text_to_image"},
|
|
140
|
+
# "Sandbox Library": {"griptape_nodes.node_libraries.sandbox.my_custom_node"}
|
|
141
|
+
#
|
|
142
|
+
_dynamic_to_stable_module_mapping: dict[str, str] # dynamic_module_name -> stable_namespace
|
|
143
|
+
_stable_to_dynamic_module_mapping: dict[str, str] # stable_namespace -> dynamic_module_name
|
|
144
|
+
_library_to_stable_modules: dict[str, set[str]] # library_name -> set of stable_namespaces
|
|
145
|
+
|
|
119
146
|
def __init__(self, event_manager: EventManager) -> None:
|
|
120
147
|
self._library_file_path_to_info = {}
|
|
148
|
+
self._dynamic_to_stable_module_mapping = {}
|
|
149
|
+
self._stable_to_dynamic_module_mapping = {}
|
|
150
|
+
self._library_to_stable_modules = {}
|
|
121
151
|
|
|
122
152
|
event_manager.assign_manager_to_request_type(
|
|
123
153
|
ListRegisteredLibrariesRequest, self.on_list_registered_libraries_request
|
|
@@ -129,6 +159,10 @@ class LibraryManager:
|
|
|
129
159
|
GetNodeMetadataFromLibraryRequest,
|
|
130
160
|
self.get_node_metadata_from_library_request,
|
|
131
161
|
)
|
|
162
|
+
event_manager.assign_manager_to_request_type(
|
|
163
|
+
LoadLibraryMetadataFromFileRequest,
|
|
164
|
+
self.load_library_metadata_from_file_request,
|
|
165
|
+
)
|
|
132
166
|
event_manager.assign_manager_to_request_type(
|
|
133
167
|
RegisterLibraryFromFileRequest,
|
|
134
168
|
self.register_library_from_file_request,
|
|
@@ -148,6 +182,9 @@ class LibraryManager:
|
|
|
148
182
|
event_manager.assign_manager_to_request_type(
|
|
149
183
|
GetAllInfoForAllLibrariesRequest, self.get_all_info_for_all_libraries_request
|
|
150
184
|
)
|
|
185
|
+
event_manager.assign_manager_to_request_type(
|
|
186
|
+
LoadMetadataForAllLibrariesRequest, self.load_metadata_for_all_libraries_request
|
|
187
|
+
)
|
|
151
188
|
event_manager.assign_manager_to_request_type(
|
|
152
189
|
UnloadLibraryFromRegistryRequest, self.unload_library_from_registry_request
|
|
153
190
|
)
|
|
@@ -295,6 +332,241 @@ class LibraryManager:
|
|
|
295
332
|
result = GetLibraryMetadataResultSuccess(metadata=metadata)
|
|
296
333
|
return result
|
|
297
334
|
|
|
335
|
+
def load_library_metadata_from_file_request(self, request: LoadLibraryMetadataFromFileRequest) -> ResultPayload:
|
|
336
|
+
"""Load library metadata from a JSON file without loading the actual node modules.
|
|
337
|
+
|
|
338
|
+
This method provides a lightweight way to get library schema information
|
|
339
|
+
without the overhead of dynamically importing Python modules.
|
|
340
|
+
"""
|
|
341
|
+
file_path = request.file_path
|
|
342
|
+
|
|
343
|
+
# Convert to Path object if it's a string
|
|
344
|
+
json_path = Path(file_path)
|
|
345
|
+
|
|
346
|
+
# Check if the file exists
|
|
347
|
+
if not json_path.exists():
|
|
348
|
+
details = f"Attempted to load Library JSON file. Failed because no file could be found at the specified path: {json_path}"
|
|
349
|
+
logger.error(details)
|
|
350
|
+
return LoadLibraryMetadataFromFileResultFailure(
|
|
351
|
+
library_path=file_path,
|
|
352
|
+
library_name=None,
|
|
353
|
+
status=LibraryManager.LibraryStatus.MISSING,
|
|
354
|
+
problems=[
|
|
355
|
+
"Library could not be found at the file path specified. It will be removed from the configuration."
|
|
356
|
+
],
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Load the JSON
|
|
360
|
+
try:
|
|
361
|
+
with json_path.open("r", encoding="utf-8") as f:
|
|
362
|
+
library_json = json.load(f)
|
|
363
|
+
except json.JSONDecodeError:
|
|
364
|
+
details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' was improperly formatted."
|
|
365
|
+
logger.error(details)
|
|
366
|
+
return LoadLibraryMetadataFromFileResultFailure(
|
|
367
|
+
library_path=file_path,
|
|
368
|
+
library_name=None,
|
|
369
|
+
status=LibraryManager.LibraryStatus.UNUSABLE,
|
|
370
|
+
problems=["Library file not formatted as proper JSON."],
|
|
371
|
+
)
|
|
372
|
+
except Exception as err:
|
|
373
|
+
details = f"Attempted to load Library JSON file from location '{json_path}'. Failed because an exception occurred: {err}"
|
|
374
|
+
logger.error(details)
|
|
375
|
+
return LoadLibraryMetadataFromFileResultFailure(
|
|
376
|
+
library_path=file_path,
|
|
377
|
+
library_name=None,
|
|
378
|
+
status=LibraryManager.LibraryStatus.UNUSABLE,
|
|
379
|
+
problems=[f"Exception occurred when attempting to load the library: {err}."],
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Try to extract library name from JSON for better error reporting
|
|
383
|
+
library_name = library_json.get("name") if isinstance(library_json, dict) else None
|
|
384
|
+
|
|
385
|
+
# Do you comport, my dude
|
|
386
|
+
try:
|
|
387
|
+
library_data = LibrarySchema.model_validate(library_json)
|
|
388
|
+
except ValidationError as err:
|
|
389
|
+
# Do some more hardcore error handling.
|
|
390
|
+
problems = []
|
|
391
|
+
for error in err.errors():
|
|
392
|
+
loc = " -> ".join(map(str, error["loc"]))
|
|
393
|
+
msg = error["msg"]
|
|
394
|
+
error_type = error["type"]
|
|
395
|
+
problem = f"Error in section '{loc}': {error_type}, {msg}"
|
|
396
|
+
problems.append(problem)
|
|
397
|
+
details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' failed to match the library schema due to: {err}"
|
|
398
|
+
logger.error(details)
|
|
399
|
+
return LoadLibraryMetadataFromFileResultFailure(
|
|
400
|
+
library_path=file_path,
|
|
401
|
+
library_name=library_name,
|
|
402
|
+
status=LibraryManager.LibraryStatus.UNUSABLE,
|
|
403
|
+
problems=problems,
|
|
404
|
+
)
|
|
405
|
+
except Exception as err:
|
|
406
|
+
details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' failed to match the library schema due to: {err}"
|
|
407
|
+
logger.error(details)
|
|
408
|
+
return LoadLibraryMetadataFromFileResultFailure(
|
|
409
|
+
library_path=file_path,
|
|
410
|
+
library_name=library_name,
|
|
411
|
+
status=LibraryManager.LibraryStatus.UNUSABLE,
|
|
412
|
+
problems=[f"Library file did not match the library schema specified due to: {err}"],
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
details = f"Successfully loaded library metadata from JSON file at {json_path}"
|
|
416
|
+
logger.debug(details)
|
|
417
|
+
return LoadLibraryMetadataFromFileResultSuccess(library_schema=library_data, file_path=file_path)
|
|
418
|
+
|
|
419
|
+
def load_metadata_for_all_libraries_request(self, request: LoadMetadataForAllLibrariesRequest) -> ResultPayload: # noqa: ARG002
|
|
420
|
+
"""Load metadata for all libraries from configuration without loading node modules.
|
|
421
|
+
|
|
422
|
+
This loads metadata from both library JSON files specified in configuration
|
|
423
|
+
and generates sandbox library metadata by scanning Python files without importing them.
|
|
424
|
+
"""
|
|
425
|
+
successful_libraries = []
|
|
426
|
+
failed_libraries = []
|
|
427
|
+
|
|
428
|
+
# Load metadata from config libraries
|
|
429
|
+
config_mgr = GriptapeNodes.ConfigManager()
|
|
430
|
+
user_libraries_section = "app_events.on_app_initialization_complete.libraries_to_register"
|
|
431
|
+
libraries_to_register: list[str] = config_mgr.get_config_value(user_libraries_section)
|
|
432
|
+
|
|
433
|
+
if libraries_to_register is not None:
|
|
434
|
+
for library_to_register in libraries_to_register:
|
|
435
|
+
if library_to_register and library_to_register.endswith(".json"):
|
|
436
|
+
# Load metadata for this library file
|
|
437
|
+
metadata_request = LoadLibraryMetadataFromFileRequest(file_path=library_to_register)
|
|
438
|
+
metadata_result = self.load_library_metadata_from_file_request(metadata_request)
|
|
439
|
+
|
|
440
|
+
if isinstance(metadata_result, LoadLibraryMetadataFromFileResultSuccess):
|
|
441
|
+
successful_libraries.append(metadata_result)
|
|
442
|
+
else:
|
|
443
|
+
failed_libraries.append(cast("LoadLibraryMetadataFromFileResultFailure", metadata_result))
|
|
444
|
+
# Note: We skip requirement specifier libraries (non-.json) as they don't have
|
|
445
|
+
# JSON files we can load metadata from without installation
|
|
446
|
+
|
|
447
|
+
# Generate sandbox library metadata
|
|
448
|
+
sandbox_result = self._generate_sandbox_library_metadata()
|
|
449
|
+
if isinstance(sandbox_result, LoadLibraryMetadataFromFileResultSuccess):
|
|
450
|
+
successful_libraries.append(sandbox_result)
|
|
451
|
+
elif isinstance(sandbox_result, LoadLibraryMetadataFromFileResultFailure):
|
|
452
|
+
failed_libraries.append(sandbox_result)
|
|
453
|
+
# If sandbox_result is None, sandbox was not configured or no files found - skip it
|
|
454
|
+
|
|
455
|
+
details = (
|
|
456
|
+
f"Successfully loaded metadata for {len(successful_libraries)} libraries, {len(failed_libraries)} failed"
|
|
457
|
+
)
|
|
458
|
+
logger.debug(details)
|
|
459
|
+
return LoadMetadataForAllLibrariesResultSuccess(
|
|
460
|
+
successful_libraries=successful_libraries,
|
|
461
|
+
failed_libraries=failed_libraries,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
def _generate_sandbox_library_metadata(
|
|
465
|
+
self,
|
|
466
|
+
) -> LoadLibraryMetadataFromFileResultSuccess | LoadLibraryMetadataFromFileResultFailure | None:
|
|
467
|
+
"""Generate sandbox library metadata by scanning Python files without importing them.
|
|
468
|
+
|
|
469
|
+
Returns None if no sandbox directory is configured or no files are found.
|
|
470
|
+
"""
|
|
471
|
+
config_mgr = GriptapeNodes.ConfigManager()
|
|
472
|
+
sandbox_library_subdir = config_mgr.get_config_value("sandbox_library_directory")
|
|
473
|
+
if not sandbox_library_subdir:
|
|
474
|
+
logger.debug("No sandbox directory specified in config. Skipping sandbox library metadata generation.")
|
|
475
|
+
return None
|
|
476
|
+
|
|
477
|
+
# Prepend the workflow directory; if the sandbox dir starts with a slash, the workflow dir will be ignored.
|
|
478
|
+
sandbox_library_dir = config_mgr.workspace_path / sandbox_library_subdir
|
|
479
|
+
sandbox_library_dir_as_posix = sandbox_library_dir.as_posix()
|
|
480
|
+
|
|
481
|
+
if not sandbox_library_dir.exists():
|
|
482
|
+
return LoadLibraryMetadataFromFileResultFailure(
|
|
483
|
+
library_path=sandbox_library_dir_as_posix,
|
|
484
|
+
library_name=LibraryManager.SANDBOX_LIBRARY_NAME,
|
|
485
|
+
status=LibraryManager.LibraryStatus.MISSING,
|
|
486
|
+
problems=["Sandbox directory does not exist."],
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
sandbox_node_candidates = self._find_files_in_dir(directory=sandbox_library_dir, extension=".py")
|
|
490
|
+
if not sandbox_node_candidates:
|
|
491
|
+
logger.debug(
|
|
492
|
+
"No candidate files found in sandbox directory '%s'. Skipping sandbox library metadata generation.",
|
|
493
|
+
sandbox_library_dir,
|
|
494
|
+
)
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
# For metadata-only generation, we create placeholder node definitions
|
|
498
|
+
# based on file names since we can't inspect the classes without importing
|
|
499
|
+
node_definitions = []
|
|
500
|
+
for candidate in sandbox_node_candidates:
|
|
501
|
+
# Use the full file name (with extension) as a placeholder to make it clear this is a file candidate
|
|
502
|
+
file_name = candidate.name
|
|
503
|
+
|
|
504
|
+
# Create a placeholder node definition - we can't get the actual class metadata
|
|
505
|
+
# without importing, so we use defaults
|
|
506
|
+
node_metadata = NodeMetadata(
|
|
507
|
+
category="Griptape Nodes Sandbox",
|
|
508
|
+
description=f"'{file_name}' may contain one or more nodes defined in this candidate file.",
|
|
509
|
+
display_name=file_name,
|
|
510
|
+
icon="square-dashed",
|
|
511
|
+
color=None,
|
|
512
|
+
)
|
|
513
|
+
node_definition = NodeDefinition(
|
|
514
|
+
class_name=file_name,
|
|
515
|
+
file_path=str(candidate),
|
|
516
|
+
metadata=node_metadata,
|
|
517
|
+
)
|
|
518
|
+
node_definitions.append(node_definition)
|
|
519
|
+
|
|
520
|
+
if not node_definitions:
|
|
521
|
+
logger.debug(
|
|
522
|
+
"No valid node files found in sandbox directory '%s'. Skipping sandbox library metadata generation.",
|
|
523
|
+
sandbox_library_dir,
|
|
524
|
+
)
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
# Create the library schema
|
|
528
|
+
sandbox_category = CategoryDefinition(
|
|
529
|
+
title="Sandbox",
|
|
530
|
+
description=f"Nodes loaded from the {LibraryManager.SANDBOX_LIBRARY_NAME}.",
|
|
531
|
+
color="#c7621a",
|
|
532
|
+
icon="Folder",
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
engine_version = GriptapeNodes().handle_engine_version_request(request=GetEngineVersionRequest())
|
|
536
|
+
if not isinstance(engine_version, GetEngineVersionResultSuccess):
|
|
537
|
+
return LoadLibraryMetadataFromFileResultFailure(
|
|
538
|
+
library_path=sandbox_library_dir_as_posix,
|
|
539
|
+
library_name=LibraryManager.SANDBOX_LIBRARY_NAME,
|
|
540
|
+
status=LibraryManager.LibraryStatus.UNUSABLE,
|
|
541
|
+
problems=["Could not get engine version for sandbox library generation."],
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
engine_version_str = f"{engine_version.major}.{engine_version.minor}.{engine_version.patch}"
|
|
545
|
+
library_metadata = LibraryMetadata(
|
|
546
|
+
author="Author needs to be specified when library is published.",
|
|
547
|
+
description="Nodes loaded from the sandbox library.",
|
|
548
|
+
library_version=engine_version_str,
|
|
549
|
+
engine_version=engine_version_str,
|
|
550
|
+
tags=["sandbox"],
|
|
551
|
+
is_griptape_nodes_searchable=False,
|
|
552
|
+
)
|
|
553
|
+
categories = [
|
|
554
|
+
{"Griptape Nodes Sandbox": sandbox_category},
|
|
555
|
+
]
|
|
556
|
+
library_schema = LibrarySchema(
|
|
557
|
+
name=LibraryManager.SANDBOX_LIBRARY_NAME,
|
|
558
|
+
library_schema_version=LibrarySchema.LATEST_SCHEMA_VERSION,
|
|
559
|
+
metadata=library_metadata,
|
|
560
|
+
categories=categories,
|
|
561
|
+
nodes=node_definitions,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
details = f"Successfully generated sandbox library metadata with {len(node_definitions)} nodes from {sandbox_library_dir}"
|
|
565
|
+
logger.debug(details)
|
|
566
|
+
return LoadLibraryMetadataFromFileResultSuccess(
|
|
567
|
+
library_schema=library_schema, file_path=str(sandbox_library_dir)
|
|
568
|
+
)
|
|
569
|
+
|
|
298
570
|
def get_node_metadata_from_library_request(self, request: GetNodeMetadataFromLibraryRequest) -> ResultPayload:
|
|
299
571
|
# Does this library exist?
|
|
300
572
|
try:
|
|
@@ -358,63 +630,25 @@ class LibraryManager:
|
|
|
358
630
|
logger.error(details)
|
|
359
631
|
return RegisterLibraryFromFileResultFailure()
|
|
360
632
|
|
|
361
|
-
#
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
library_name=None,
|
|
369
|
-
status=LibraryManager.LibraryStatus.UNUSABLE,
|
|
370
|
-
problems=["Library file not formatted as proper JSON."],
|
|
371
|
-
)
|
|
372
|
-
details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' was improperly formatted."
|
|
373
|
-
logger.error(details)
|
|
374
|
-
return RegisterLibraryFromFileResultFailure()
|
|
375
|
-
except Exception as err:
|
|
376
|
-
self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
|
|
377
|
-
library_path=file_path,
|
|
378
|
-
library_name=None,
|
|
379
|
-
status=LibraryManager.LibraryStatus.UNUSABLE,
|
|
380
|
-
problems=[f"Exception occurred when attempting to load the library: {err}."],
|
|
381
|
-
)
|
|
382
|
-
details = f"Attempted to load Library JSON file from location '{json_path}'. Failed because an exception occurred: {err}"
|
|
383
|
-
logger.error(details)
|
|
384
|
-
return RegisterLibraryFromFileResultFailure()
|
|
633
|
+
# Use the new metadata loading functionality
|
|
634
|
+
metadata_request = LoadLibraryMetadataFromFileRequest(file_path=file_path)
|
|
635
|
+
metadata_result = self.load_library_metadata_from_file_request(metadata_request)
|
|
636
|
+
|
|
637
|
+
if not isinstance(metadata_result, LoadLibraryMetadataFromFileResultSuccess):
|
|
638
|
+
# Metadata loading failed, use the detailed error information from the failure result
|
|
639
|
+
failure_result = cast("LoadLibraryMetadataFromFileResultFailure", metadata_result)
|
|
385
640
|
|
|
386
|
-
# Do you comport, my dude
|
|
387
|
-
try:
|
|
388
|
-
library_data = LibrarySchema.model_validate(library_json)
|
|
389
|
-
except ValidationError as err:
|
|
390
|
-
# Do some more hardcore error handling.
|
|
391
|
-
problems = []
|
|
392
|
-
for error in err.errors():
|
|
393
|
-
loc = " -> ".join(map(str, error["loc"]))
|
|
394
|
-
msg = error["msg"]
|
|
395
|
-
error_type = error["type"]
|
|
396
|
-
problem = f"Error in section '{loc}': {error_type}, {msg}"
|
|
397
|
-
problems.append(problem)
|
|
398
|
-
self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
|
|
399
|
-
library_path=file_path,
|
|
400
|
-
library_name=None,
|
|
401
|
-
status=LibraryManager.LibraryStatus.UNUSABLE,
|
|
402
|
-
problems=problems,
|
|
403
|
-
)
|
|
404
|
-
details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' failed to match the library schema due to: {err}"
|
|
405
|
-
logger.error(details)
|
|
406
|
-
return RegisterLibraryFromFileResultFailure()
|
|
407
|
-
except Exception as err:
|
|
408
641
|
self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
|
|
409
642
|
library_path=file_path,
|
|
410
|
-
library_name=
|
|
411
|
-
status=
|
|
412
|
-
problems=
|
|
643
|
+
library_name=failure_result.library_name,
|
|
644
|
+
status=failure_result.status,
|
|
645
|
+
problems=failure_result.problems,
|
|
413
646
|
)
|
|
414
|
-
details = f"Attempted to load Library JSON file. Failed because the file at path '{json_path}' failed to match the library schema due to: {err}"
|
|
415
|
-
logger.error(details)
|
|
416
647
|
return RegisterLibraryFromFileResultFailure()
|
|
417
648
|
|
|
649
|
+
# Get the validated library data
|
|
650
|
+
library_data = metadata_result.library_schema
|
|
651
|
+
|
|
418
652
|
# Make sure the version string is copacetic.
|
|
419
653
|
library_version = library_data.metadata.library_version
|
|
420
654
|
if library_version is None:
|
|
@@ -435,12 +669,35 @@ class LibraryManager:
|
|
|
435
669
|
# Add the directory to the Python path to allow for relative imports
|
|
436
670
|
sys.path.insert(0, str(base_dir))
|
|
437
671
|
|
|
672
|
+
# Load the advanced library module if specified
|
|
673
|
+
advanced_library_instance = None
|
|
674
|
+
if library_data.advanced_library_path:
|
|
675
|
+
try:
|
|
676
|
+
advanced_library_instance = self._load_advanced_library_module(
|
|
677
|
+
library_data=library_data,
|
|
678
|
+
base_dir=base_dir,
|
|
679
|
+
)
|
|
680
|
+
except Exception as err:
|
|
681
|
+
self._library_file_path_to_info[file_path] = LibraryManager.LibraryInfo(
|
|
682
|
+
library_path=file_path,
|
|
683
|
+
library_name=library_data.name,
|
|
684
|
+
library_version=library_version,
|
|
685
|
+
status=LibraryManager.LibraryStatus.UNUSABLE,
|
|
686
|
+
problems=[
|
|
687
|
+
f"Failed to load Advanced Library module from '{library_data.advanced_library_path}': {err}"
|
|
688
|
+
],
|
|
689
|
+
)
|
|
690
|
+
details = f"Attempted to load Library '{library_data.name}' from '{json_path}'. Failed to load Advanced Library module: {err}"
|
|
691
|
+
logger.error(details)
|
|
692
|
+
return RegisterLibraryFromFileResultFailure()
|
|
693
|
+
|
|
438
694
|
# Create or get the library
|
|
439
695
|
try:
|
|
440
696
|
# Try to create a new library
|
|
441
697
|
library = LibraryRegistry.generate_new_library(
|
|
442
698
|
library_data=library_data,
|
|
443
699
|
mark_as_default_library=request.load_as_default_library,
|
|
700
|
+
advanced_library=advanced_library_instance,
|
|
444
701
|
)
|
|
445
702
|
|
|
446
703
|
except KeyError as err:
|
|
@@ -593,8 +850,8 @@ class LibraryManager:
|
|
|
593
850
|
def register_library_from_requirement_specifier_request(
|
|
594
851
|
self, request: RegisterLibraryFromRequirementSpecifierRequest
|
|
595
852
|
) -> ResultPayload:
|
|
596
|
-
package_name = Requirement(request.requirement_specifier).name
|
|
597
853
|
try:
|
|
854
|
+
package_name = Requirement(request.requirement_specifier).name
|
|
598
855
|
# Determine venv path for dependency installation
|
|
599
856
|
venv_path = self._get_library_venv_path(package_name, None)
|
|
600
857
|
|
|
@@ -630,6 +887,10 @@ class LibraryManager:
|
|
|
630
887
|
details = f"Attempted to install library '{request.requirement_specifier}'. Failed: return code={e.returncode}, stdout={e.stdout}, stderr={e.stderr}"
|
|
631
888
|
logger.error(details)
|
|
632
889
|
return RegisterLibraryFromRequirementSpecifierResultFailure()
|
|
890
|
+
except InvalidRequirement as e:
|
|
891
|
+
details = f"Attempted to install library '{request.requirement_specifier}'. Failed due to invalid requirement specifier: {e}"
|
|
892
|
+
logger.error(details)
|
|
893
|
+
return RegisterLibraryFromRequirementSpecifierResultFailure()
|
|
633
894
|
|
|
634
895
|
library_path = str(files(package_name).joinpath(request.library_config_name))
|
|
635
896
|
|
|
@@ -752,6 +1013,9 @@ class LibraryManager:
|
|
|
752
1013
|
logger.error(details)
|
|
753
1014
|
return UnloadLibraryFromRegistryResultFailure()
|
|
754
1015
|
|
|
1016
|
+
# Clean up all stable module aliases for this library
|
|
1017
|
+
self._unregister_all_stable_module_aliases_for_library(request.library_name)
|
|
1018
|
+
|
|
755
1019
|
# Remove the library from our library info list. This prevents it from still showing
|
|
756
1020
|
# up in the table of attempted library loads.
|
|
757
1021
|
lib_info = self.get_library_info_by_library_name(request.library_name)
|
|
@@ -876,11 +1140,149 @@ class LibraryManager:
|
|
|
876
1140
|
)
|
|
877
1141
|
return result
|
|
878
1142
|
|
|
879
|
-
def
|
|
1143
|
+
def _create_stable_namespace(self, library_name: str, file_path: Path) -> str:
|
|
1144
|
+
"""Create a stable namespace for a dynamic module.
|
|
1145
|
+
|
|
1146
|
+
Args:
|
|
1147
|
+
library_name: Name of the library
|
|
1148
|
+
file_path: Path to the Python file
|
|
1149
|
+
|
|
1150
|
+
Returns:
|
|
1151
|
+
Stable namespace string like 'griptape_nodes.node_libraries.runwayml_library.image_to_video'
|
|
1152
|
+
"""
|
|
1153
|
+
# Convert library name to safe module name
|
|
1154
|
+
safe_library_name = library_name.lower().replace(" ", "_").replace("-", "_")
|
|
1155
|
+
# Remove invalid characters
|
|
1156
|
+
safe_library_name = "".join(c for c in safe_library_name if c.isalnum() or c == "_")
|
|
1157
|
+
|
|
1158
|
+
# Convert file path to safe module name
|
|
1159
|
+
safe_file_name = file_path.stem.replace("-", "_")
|
|
1160
|
+
|
|
1161
|
+
return f"griptape_nodes.node_libraries.{safe_library_name}.{safe_file_name}"
|
|
1162
|
+
|
|
1163
|
+
def _register_stable_module_alias(
|
|
1164
|
+
self, dynamic_module_name: str, stable_namespace: str, module: ModuleType, library_name: str
|
|
1165
|
+
) -> None:
|
|
1166
|
+
"""Register a stable alias for a dynamic module in sys.modules.
|
|
1167
|
+
|
|
1168
|
+
Args:
|
|
1169
|
+
dynamic_module_name: Original dynamic module name
|
|
1170
|
+
stable_namespace: Stable namespace to alias to
|
|
1171
|
+
module: The loaded module
|
|
1172
|
+
library_name: Name of the library
|
|
1173
|
+
"""
|
|
1174
|
+
# Update our mapping
|
|
1175
|
+
self._dynamic_to_stable_module_mapping[dynamic_module_name] = stable_namespace
|
|
1176
|
+
self._stable_to_dynamic_module_mapping[stable_namespace] = dynamic_module_name
|
|
1177
|
+
|
|
1178
|
+
# Track library-to-modules mapping for bulk cleanup
|
|
1179
|
+
library_key = library_name
|
|
1180
|
+
if library_key not in self._library_to_stable_modules:
|
|
1181
|
+
self._library_to_stable_modules[library_key] = set()
|
|
1182
|
+
self._library_to_stable_modules[library_key].add(stable_namespace)
|
|
1183
|
+
|
|
1184
|
+
# Register the stable alias in sys.modules
|
|
1185
|
+
sys.modules[stable_namespace] = module
|
|
1186
|
+
|
|
1187
|
+
details = f"Registered stable alias: {stable_namespace} -> {dynamic_module_name} (library: {library_key})"
|
|
1188
|
+
logger.debug(details)
|
|
1189
|
+
|
|
1190
|
+
def _unregister_stable_module_alias(self, dynamic_module_name: str) -> None:
|
|
1191
|
+
"""Unregister a stable alias for a dynamic module during hot reload.
|
|
1192
|
+
|
|
1193
|
+
Args:
|
|
1194
|
+
dynamic_module_name: Original dynamic module name
|
|
1195
|
+
"""
|
|
1196
|
+
if dynamic_module_name in self._dynamic_to_stable_module_mapping:
|
|
1197
|
+
stable_namespace = self._dynamic_to_stable_module_mapping[dynamic_module_name]
|
|
1198
|
+
|
|
1199
|
+
# Remove from sys.modules if it exists
|
|
1200
|
+
if stable_namespace in sys.modules:
|
|
1201
|
+
del sys.modules[stable_namespace]
|
|
1202
|
+
|
|
1203
|
+
# Remove from library tracking
|
|
1204
|
+
for library_modules in self._library_to_stable_modules.values():
|
|
1205
|
+
library_modules.discard(stable_namespace)
|
|
1206
|
+
|
|
1207
|
+
# Remove from our mappings
|
|
1208
|
+
del self._dynamic_to_stable_module_mapping[dynamic_module_name]
|
|
1209
|
+
del self._stable_to_dynamic_module_mapping[stable_namespace]
|
|
1210
|
+
|
|
1211
|
+
details = f"Unregistered stable alias: {stable_namespace}"
|
|
1212
|
+
logger.debug(details)
|
|
1213
|
+
|
|
1214
|
+
def _unregister_all_stable_module_aliases_for_library(self, library_name: str) -> None:
|
|
1215
|
+
"""Unregister all stable module aliases for a library during library unload/reload.
|
|
1216
|
+
|
|
1217
|
+
Args:
|
|
1218
|
+
library_name: Name of the library to clean up
|
|
1219
|
+
"""
|
|
1220
|
+
library_key = library_name
|
|
1221
|
+
if library_key not in self._library_to_stable_modules:
|
|
1222
|
+
return
|
|
1223
|
+
|
|
1224
|
+
stable_namespaces = self._library_to_stable_modules[library_key].copy()
|
|
1225
|
+
details = f"Unregistering {len(stable_namespaces)} stable aliases for library: {library_name}"
|
|
1226
|
+
logger.debug(details)
|
|
1227
|
+
|
|
1228
|
+
for stable_namespace in stable_namespaces:
|
|
1229
|
+
# Remove from sys.modules if it exists
|
|
1230
|
+
if stable_namespace in sys.modules:
|
|
1231
|
+
del sys.modules[stable_namespace]
|
|
1232
|
+
|
|
1233
|
+
# Find and remove from dynamic mapping
|
|
1234
|
+
dynamic_module_name = self._stable_to_dynamic_module_mapping.get(stable_namespace)
|
|
1235
|
+
if dynamic_module_name:
|
|
1236
|
+
self._dynamic_to_stable_module_mapping.pop(dynamic_module_name, None)
|
|
1237
|
+
self._stable_to_dynamic_module_mapping.pop(stable_namespace, None)
|
|
1238
|
+
|
|
1239
|
+
# Clear the library's module set
|
|
1240
|
+
del self._library_to_stable_modules[library_key]
|
|
1241
|
+
details = f"Completed cleanup of stable aliases for library: '{library_name}'."
|
|
1242
|
+
logger.debug(details)
|
|
1243
|
+
|
|
1244
|
+
def get_stable_namespace_for_dynamic_module(self, dynamic_module_name: str) -> str | None:
|
|
1245
|
+
"""Get the stable namespace for a dynamic module name.
|
|
1246
|
+
|
|
1247
|
+
This method is used during workflow serialization to convert dynamic module names
|
|
1248
|
+
(like "gtn_dynamic_module_image_to_video_py_123456789") to stable namespace imports
|
|
1249
|
+
(like "griptape_nodes.node_libraries.runwayml_library.image_to_video").
|
|
1250
|
+
|
|
1251
|
+
Args:
|
|
1252
|
+
dynamic_module_name: The dynamic module name to look up
|
|
1253
|
+
|
|
1254
|
+
Returns:
|
|
1255
|
+
The stable namespace string, or None if not found
|
|
1256
|
+
|
|
1257
|
+
Example:
|
|
1258
|
+
>>> manager.get_stable_namespace_for_dynamic_module("gtn_dynamic_module_image_to_video_py_123456789")
|
|
1259
|
+
"griptape_nodes.node_libraries.runwayml_library.image_to_video"
|
|
1260
|
+
"""
|
|
1261
|
+
return self._dynamic_to_stable_module_mapping.get(dynamic_module_name)
|
|
1262
|
+
|
|
1263
|
+
def is_dynamic_module(self, module_name: str) -> bool:
|
|
1264
|
+
"""Check if a module name represents a dynamically loaded module.
|
|
1265
|
+
|
|
1266
|
+
Args:
|
|
1267
|
+
module_name: The module name to check
|
|
1268
|
+
|
|
1269
|
+
Returns:
|
|
1270
|
+
True if this is a dynamic module name, False otherwise
|
|
1271
|
+
|
|
1272
|
+
Example:
|
|
1273
|
+
>>> manager.is_dynamic_module("gtn_dynamic_module_image_to_video_py_123456789")
|
|
1274
|
+
True
|
|
1275
|
+
>>> manager.is_dynamic_module("griptape.artifacts")
|
|
1276
|
+
False
|
|
1277
|
+
"""
|
|
1278
|
+
return module_name.startswith("gtn_dynamic_module_")
|
|
1279
|
+
|
|
1280
|
+
def _load_module_from_file(self, file_path: Path | str, library_name: str) -> ModuleType:
|
|
880
1281
|
"""Dynamically load a module from a Python file with support for hot reloading.
|
|
881
1282
|
|
|
882
1283
|
Args:
|
|
883
1284
|
file_path: Path to the Python file
|
|
1285
|
+
library_name: Name of the library
|
|
884
1286
|
|
|
885
1287
|
Returns:
|
|
886
1288
|
The loaded module
|
|
@@ -892,13 +1294,19 @@ class LibraryManager:
|
|
|
892
1294
|
file_path = Path(file_path)
|
|
893
1295
|
|
|
894
1296
|
# Generate a unique module name
|
|
895
|
-
module_name = f"
|
|
1297
|
+
module_name = f"gtn_dynamic_module_{file_path.name.replace('.', '_')}_{hash(str(file_path))}"
|
|
1298
|
+
|
|
1299
|
+
# Create stable namespace
|
|
1300
|
+
stable_namespace = self._create_stable_namespace(library_name, file_path)
|
|
896
1301
|
|
|
897
1302
|
# Check if this module is already loaded
|
|
898
1303
|
if module_name in sys.modules:
|
|
899
1304
|
# For dynamically loaded modules, we need to re-create the module
|
|
900
1305
|
# with a fresh spec rather than using importlib.reload
|
|
901
1306
|
|
|
1307
|
+
# Unregister old stable alias
|
|
1308
|
+
self._unregister_stable_module_alias(module_name)
|
|
1309
|
+
|
|
902
1310
|
# Remove the old module from sys.modules
|
|
903
1311
|
old_module = sys.modules.pop(module_name)
|
|
904
1312
|
|
|
@@ -914,6 +1322,8 @@ class LibraryManager:
|
|
|
914
1322
|
try:
|
|
915
1323
|
# Execute the module with the new code
|
|
916
1324
|
spec.loader.exec_module(module)
|
|
1325
|
+
# Register new stable alias
|
|
1326
|
+
self._register_stable_module_alias(module_name, stable_namespace, module, library_name)
|
|
917
1327
|
details = f"Hot reloaded module: {module_name} from {file_path}"
|
|
918
1328
|
logger.debug(details)
|
|
919
1329
|
except Exception as e:
|
|
@@ -939,18 +1349,21 @@ class LibraryManager:
|
|
|
939
1349
|
# Execute the module
|
|
940
1350
|
try:
|
|
941
1351
|
spec.loader.exec_module(module)
|
|
1352
|
+
# Register stable alias
|
|
1353
|
+
self._register_stable_module_alias(module_name, stable_namespace, module, library_name)
|
|
942
1354
|
except Exception as err:
|
|
943
1355
|
msg = f"Module at '{file_path}' failed to load with error: {err}"
|
|
944
1356
|
raise ImportError(msg) from err
|
|
945
1357
|
|
|
946
1358
|
return module
|
|
947
1359
|
|
|
948
|
-
def _load_class_from_file(self, file_path: Path | str, class_name: str) -> type[BaseNode]:
|
|
1360
|
+
def _load_class_from_file(self, file_path: Path | str, class_name: str, library_name: str) -> type[BaseNode]:
|
|
949
1361
|
"""Dynamically load a class from a Python file with support for hot reloading.
|
|
950
1362
|
|
|
951
1363
|
Args:
|
|
952
1364
|
file_path: Path to the Python file
|
|
953
1365
|
class_name: Name of the class to load
|
|
1366
|
+
library_name: Name of the library
|
|
954
1367
|
|
|
955
1368
|
Returns:
|
|
956
1369
|
The loaded class
|
|
@@ -961,7 +1374,7 @@ class LibraryManager:
|
|
|
961
1374
|
TypeError: If the loaded class isn't a BaseNode-derived class
|
|
962
1375
|
"""
|
|
963
1376
|
try:
|
|
964
|
-
module = self._load_module_from_file(file_path)
|
|
1377
|
+
module = self._load_module_from_file(file_path, library_name)
|
|
965
1378
|
except ImportError as err:
|
|
966
1379
|
msg = f"Attempted to load class '{class_name}'. Error: {err}"
|
|
967
1380
|
raise ImportError(msg) from err
|
|
@@ -981,16 +1394,50 @@ class LibraryManager:
|
|
|
981
1394
|
return node_class
|
|
982
1395
|
|
|
983
1396
|
def load_all_libraries_from_config(self) -> None:
|
|
1397
|
+
# Load metadata for all libraries to determine which ones can be safely loaded
|
|
1398
|
+
metadata_request = LoadMetadataForAllLibrariesRequest()
|
|
1399
|
+
metadata_result = self.load_metadata_for_all_libraries_request(metadata_request)
|
|
1400
|
+
|
|
1401
|
+
# Check if metadata loading succeeded
|
|
1402
|
+
if not isinstance(metadata_result, LoadMetadataForAllLibrariesResultSuccess):
|
|
1403
|
+
logger.error("Failed to load metadata for all libraries, skipping library registration")
|
|
1404
|
+
return
|
|
1405
|
+
|
|
1406
|
+
# Record all failed libraries in our tracking immediately
|
|
1407
|
+
for failed_library in metadata_result.failed_libraries:
|
|
1408
|
+
self._library_file_path_to_info[failed_library.library_path] = LibraryManager.LibraryInfo(
|
|
1409
|
+
library_path=failed_library.library_path,
|
|
1410
|
+
library_name=failed_library.library_name,
|
|
1411
|
+
status=failed_library.status,
|
|
1412
|
+
problems=failed_library.problems,
|
|
1413
|
+
)
|
|
1414
|
+
|
|
1415
|
+
# Use metadata results to selectively load libraries
|
|
984
1416
|
user_libraries_section = "app_events.on_app_initialization_complete.libraries_to_register"
|
|
985
|
-
self._load_libraries_from_config_category(config_category=user_libraries_section, load_as_default_library=False)
|
|
986
1417
|
|
|
987
|
-
|
|
988
|
-
|
|
1418
|
+
# Load libraries that had successful metadata loading
|
|
1419
|
+
for library_result in metadata_result.successful_libraries:
|
|
1420
|
+
if library_result.library_schema.name == LibraryManager.SANDBOX_LIBRARY_NAME:
|
|
1421
|
+
# Handle sandbox library - use the schema we already have
|
|
1422
|
+
self._attempt_generate_sandbox_library_from_schema(
|
|
1423
|
+
library_schema=library_result.library_schema, sandbox_directory=library_result.file_path
|
|
1424
|
+
)
|
|
1425
|
+
else:
|
|
1426
|
+
# Handle config-based library - register it directly using the file path
|
|
1427
|
+
register_request = RegisterLibraryFromFileRequest(
|
|
1428
|
+
file_path=library_result.file_path, load_as_default_library=False
|
|
1429
|
+
)
|
|
1430
|
+
register_result = self.register_library_from_file_request(register_request)
|
|
1431
|
+
if isinstance(register_result, RegisterLibraryFromFileResultFailure):
|
|
1432
|
+
# Registration failed - the failure info is already recorded in _library_file_path_to_info
|
|
1433
|
+
# by register_library_from_file_request, so we just log it here for visibility
|
|
1434
|
+
logger.warning(f"Failed to register library from {library_result.file_path}") # noqa: G004
|
|
989
1435
|
|
|
990
1436
|
# Print 'em all pretty
|
|
991
1437
|
self.print_library_load_status()
|
|
992
1438
|
|
|
993
1439
|
# Remove any missing libraries AFTER we've printed them for the user.
|
|
1440
|
+
user_libraries_section = "app_events.on_app_initialization_complete.libraries_to_register"
|
|
994
1441
|
self._remove_missing_libraries_from_config(config_category=user_libraries_section)
|
|
995
1442
|
|
|
996
1443
|
def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
|
|
@@ -1032,7 +1479,73 @@ class LibraryManager:
|
|
|
1032
1479
|
# Go tell the Workflow Manager that it's turn is now.
|
|
1033
1480
|
GriptapeNodes.WorkflowManager().on_libraries_initialization_complete()
|
|
1034
1481
|
|
|
1035
|
-
def
|
|
1482
|
+
def _load_advanced_library_module(
|
|
1483
|
+
self,
|
|
1484
|
+
library_data: LibrarySchema,
|
|
1485
|
+
base_dir: Path,
|
|
1486
|
+
) -> AdvancedNodeLibrary | None:
|
|
1487
|
+
"""Load the advanced library module and return an instance.
|
|
1488
|
+
|
|
1489
|
+
Args:
|
|
1490
|
+
library_data: The library schema data
|
|
1491
|
+
base_dir: Base directory containing the library files
|
|
1492
|
+
|
|
1493
|
+
Returns:
|
|
1494
|
+
An instance of the AdvancedNodeLibrary class from the module, or None if not specified
|
|
1495
|
+
|
|
1496
|
+
Raises:
|
|
1497
|
+
ImportError: If the module cannot be loaded
|
|
1498
|
+
AttributeError: If no AdvancedNodeLibrary subclass is found
|
|
1499
|
+
TypeError: If the found class cannot be instantiated
|
|
1500
|
+
"""
|
|
1501
|
+
from griptape_nodes.node_library.advanced_node_library import AdvancedNodeLibrary
|
|
1502
|
+
|
|
1503
|
+
if not library_data.advanced_library_path:
|
|
1504
|
+
return None
|
|
1505
|
+
|
|
1506
|
+
# Resolve relative path to absolute path
|
|
1507
|
+
advanced_library_module_path = Path(library_data.advanced_library_path)
|
|
1508
|
+
if not advanced_library_module_path.is_absolute():
|
|
1509
|
+
advanced_library_module_path = base_dir / advanced_library_module_path
|
|
1510
|
+
|
|
1511
|
+
# Load the module (supports hot reloading)
|
|
1512
|
+
try:
|
|
1513
|
+
module = self._load_module_from_file(advanced_library_module_path, library_data.name)
|
|
1514
|
+
except Exception as err:
|
|
1515
|
+
msg = f"Failed to load Advanced Library module from '{advanced_library_module_path}': {err}"
|
|
1516
|
+
raise ImportError(msg) from err
|
|
1517
|
+
|
|
1518
|
+
# Find an AdvancedNodeLibrary subclass in the module
|
|
1519
|
+
advanced_library_class = None
|
|
1520
|
+
for obj in vars(module).values():
|
|
1521
|
+
if (
|
|
1522
|
+
isinstance(obj, type)
|
|
1523
|
+
and issubclass(obj, AdvancedNodeLibrary)
|
|
1524
|
+
and obj is not AdvancedNodeLibrary
|
|
1525
|
+
and obj.__module__ == module.__name__
|
|
1526
|
+
):
|
|
1527
|
+
advanced_library_class = obj
|
|
1528
|
+
break
|
|
1529
|
+
|
|
1530
|
+
if not advanced_library_class:
|
|
1531
|
+
msg = f"No AdvancedNodeLibrary subclass found in Advanced Library module '{advanced_library_module_path}'"
|
|
1532
|
+
raise AttributeError(msg)
|
|
1533
|
+
|
|
1534
|
+
# Create an instance
|
|
1535
|
+
try:
|
|
1536
|
+
advanced_library_instance = advanced_library_class()
|
|
1537
|
+
except Exception as err:
|
|
1538
|
+
msg = f"Failed to instantiate AdvancedNodeLibrary class '{advanced_library_class.__name__}': {err}"
|
|
1539
|
+
raise TypeError(msg) from err
|
|
1540
|
+
|
|
1541
|
+
# Validate the instance
|
|
1542
|
+
if not isinstance(advanced_library_instance, AdvancedNodeLibrary):
|
|
1543
|
+
msg = f"Created instance is not an AdvancedNodeLibrary subclass: {type(advanced_library_instance)}"
|
|
1544
|
+
raise TypeError(msg)
|
|
1545
|
+
|
|
1546
|
+
return advanced_library_instance
|
|
1547
|
+
|
|
1548
|
+
def _attempt_load_nodes_from_library( # noqa: PLR0913, PLR0912, PLR0915, C901
|
|
1036
1549
|
self,
|
|
1037
1550
|
library_data: LibrarySchema,
|
|
1038
1551
|
library: Library,
|
|
@@ -1061,6 +1574,21 @@ class LibraryManager:
|
|
|
1061
1574
|
problems=problems,
|
|
1062
1575
|
)
|
|
1063
1576
|
|
|
1577
|
+
# Call the before_library_nodes_loaded callback if available
|
|
1578
|
+
advanced_library = library.get_advanced_library()
|
|
1579
|
+
if advanced_library:
|
|
1580
|
+
try:
|
|
1581
|
+
advanced_library.before_library_nodes_loaded(library_data, library)
|
|
1582
|
+
details = f"Successfully called before_library_nodes_loaded callback for library '{library_data.name}'"
|
|
1583
|
+
logger.debug(details)
|
|
1584
|
+
except Exception as err:
|
|
1585
|
+
problem = f"Error calling before_library_nodes_loaded callback: {err}"
|
|
1586
|
+
problems.append(problem)
|
|
1587
|
+
details = (
|
|
1588
|
+
f"Failed to call before_library_nodes_loaded callback for library '{library_data.name}': {err}"
|
|
1589
|
+
)
|
|
1590
|
+
logger.error(details)
|
|
1591
|
+
|
|
1064
1592
|
# Process each node in the metadata
|
|
1065
1593
|
for node_definition in library_data.nodes:
|
|
1066
1594
|
# Resolve relative path to absolute path
|
|
@@ -1070,7 +1598,7 @@ class LibraryManager:
|
|
|
1070
1598
|
|
|
1071
1599
|
try:
|
|
1072
1600
|
# Dynamically load the module containing the node class
|
|
1073
|
-
node_class = self._load_class_from_file(node_file_path, node_definition.class_name)
|
|
1601
|
+
node_class = self._load_class_from_file(node_file_path, node_definition.class_name, library_data.name)
|
|
1074
1602
|
except Exception as err:
|
|
1075
1603
|
problems.append(
|
|
1076
1604
|
f"Failed to load node '{node_definition.class_name}' from '{node_file_path}' with error: {err}"
|
|
@@ -1095,6 +1623,18 @@ class LibraryManager:
|
|
|
1095
1623
|
# If we got here, at least one node came in.
|
|
1096
1624
|
any_nodes_loaded_successfully = True
|
|
1097
1625
|
|
|
1626
|
+
# Call the after_library_nodes_loaded callback if available
|
|
1627
|
+
if advanced_library:
|
|
1628
|
+
try:
|
|
1629
|
+
advanced_library.after_library_nodes_loaded(library_data, library)
|
|
1630
|
+
details = f"Successfully called after_library_nodes_loaded callback for library '{library_data.name}'"
|
|
1631
|
+
logger.debug(details)
|
|
1632
|
+
except Exception as err:
|
|
1633
|
+
problem = f"Error calling after_library_nodes_loaded callback: {err}"
|
|
1634
|
+
problems.append(problem)
|
|
1635
|
+
details = f"Failed to call after_library_nodes_loaded callback for library '{library_data.name}': {err}"
|
|
1636
|
+
logger.error(details)
|
|
1637
|
+
|
|
1098
1638
|
# Create a LibraryInfo object based on load successes and problem count.
|
|
1099
1639
|
if not any_nodes_loaded_successfully:
|
|
1100
1640
|
status = LibraryManager.LibraryStatus.UNUSABLE
|
|
@@ -1114,39 +1654,24 @@ class LibraryManager:
|
|
|
1114
1654
|
problems=problems,
|
|
1115
1655
|
)
|
|
1116
1656
|
|
|
1117
|
-
def
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
return
|
|
1123
|
-
|
|
1124
|
-
# Prepend the workflow directory; if the sandbox dir starts with a slash, the workflow dir will be ignored.
|
|
1125
|
-
sandbox_library_dir = config_mgr.workspace_path / sandbox_library_subdir
|
|
1657
|
+
def _attempt_generate_sandbox_library_from_schema(
|
|
1658
|
+
self, library_schema: LibrarySchema, sandbox_directory: str
|
|
1659
|
+
) -> None:
|
|
1660
|
+
"""Generate sandbox library using an existing schema, loading actual node classes."""
|
|
1661
|
+
sandbox_library_dir = Path(sandbox_directory)
|
|
1126
1662
|
sandbox_library_dir_as_posix = sandbox_library_dir.as_posix()
|
|
1127
1663
|
|
|
1128
|
-
sandbox_node_candidates = self._find_files_in_dir(directory=sandbox_library_dir, extension=".py")
|
|
1129
|
-
if not sandbox_node_candidates:
|
|
1130
|
-
logger.debug("No candidate files found in sandbox directory '%s'. Skipping.", sandbox_library_dir)
|
|
1131
|
-
return
|
|
1132
|
-
|
|
1133
|
-
sandbox_category = CategoryDefinition(
|
|
1134
|
-
title="Sandbox",
|
|
1135
|
-
description="Nodes loaded from the Sandbox Library.",
|
|
1136
|
-
color="#ff0000",
|
|
1137
|
-
icon="Folder",
|
|
1138
|
-
)
|
|
1139
|
-
|
|
1140
1664
|
problems = []
|
|
1141
1665
|
|
|
1142
|
-
#
|
|
1143
|
-
|
|
1144
|
-
for
|
|
1666
|
+
# Get the file paths from the schema's node definitions to load actual classes
|
|
1667
|
+
actual_node_definitions = []
|
|
1668
|
+
for node_def in library_schema.nodes:
|
|
1669
|
+
candidate_path = Path(node_def.file_path)
|
|
1145
1670
|
try:
|
|
1146
|
-
module = self._load_module_from_file(
|
|
1671
|
+
module = self._load_module_from_file(candidate_path, LibraryManager.SANDBOX_LIBRARY_NAME)
|
|
1147
1672
|
except Exception as err:
|
|
1148
|
-
problems.append(f"Could not load module in sandbox library '{
|
|
1149
|
-
details = f"Attempted to load module in sandbox library '{
|
|
1673
|
+
problems.append(f"Could not load module in sandbox library '{candidate_path}': {err}")
|
|
1674
|
+
details = f"Attempted to load module in sandbox library '{candidate_path}'. Failed because an exception occurred: {err}."
|
|
1150
1675
|
logger.warning(details)
|
|
1151
1676
|
continue # SKIP IT
|
|
1152
1677
|
|
|
@@ -1158,47 +1683,41 @@ class LibraryManager:
|
|
|
1158
1683
|
and type(obj) is not BaseNode
|
|
1159
1684
|
and obj.__module__ == module.__name__
|
|
1160
1685
|
):
|
|
1161
|
-
details = f"Found node '{class_name}' in sandbox library '{
|
|
1686
|
+
details = f"Found node '{class_name}' in sandbox library '{candidate_path}'."
|
|
1162
1687
|
logger.debug(details)
|
|
1688
|
+
|
|
1689
|
+
# Get metadata from class attributes if they exist, otherwise use defaults
|
|
1690
|
+
node_icon = getattr(obj, "ICON", "square-dashed")
|
|
1691
|
+
node_description = getattr(
|
|
1692
|
+
obj, "DESCRIPTION", f"'{class_name}' (loaded from the {LibraryManager.SANDBOX_LIBRARY_NAME})."
|
|
1693
|
+
)
|
|
1694
|
+
node_color = getattr(obj, "COLOR", None)
|
|
1695
|
+
|
|
1163
1696
|
node_metadata = NodeMetadata(
|
|
1164
1697
|
category="Griptape Nodes Sandbox",
|
|
1165
|
-
description=
|
|
1698
|
+
description=node_description,
|
|
1166
1699
|
display_name=class_name,
|
|
1700
|
+
icon=node_icon,
|
|
1701
|
+
color=node_color,
|
|
1167
1702
|
)
|
|
1168
1703
|
node_definition = NodeDefinition(
|
|
1169
1704
|
class_name=class_name,
|
|
1170
|
-
file_path=str(
|
|
1705
|
+
file_path=str(candidate_path),
|
|
1171
1706
|
metadata=node_metadata,
|
|
1172
1707
|
)
|
|
1173
|
-
|
|
1708
|
+
actual_node_definitions.append(node_definition)
|
|
1174
1709
|
|
|
1175
|
-
if not
|
|
1710
|
+
if not actual_node_definitions:
|
|
1176
1711
|
logger.info("No nodes found in sandbox library '%s'. Skipping.", sandbox_library_dir)
|
|
1177
1712
|
return
|
|
1178
1713
|
|
|
1179
|
-
#
|
|
1180
|
-
engine_version = GriptapeNodes().handle_engine_version_request(request=GetEngineVersionRequest())
|
|
1181
|
-
if not isinstance(engine_version, GetEngineVersionResultSuccess):
|
|
1182
|
-
logger.error("Could not get engine version. Skipping sandbox library.")
|
|
1183
|
-
return
|
|
1184
|
-
engine_version_str = f"{engine_version.major}.{engine_version.minor}.{engine_version.patch}"
|
|
1185
|
-
library_metadata = LibraryMetadata(
|
|
1186
|
-
author="Author needs to be specified when library is published.",
|
|
1187
|
-
description="Nodes loaded from the sandbox library.",
|
|
1188
|
-
library_version=engine_version_str,
|
|
1189
|
-
engine_version=engine_version_str,
|
|
1190
|
-
tags=["sandbox"],
|
|
1191
|
-
is_griptape_nodes_searchable=False,
|
|
1192
|
-
)
|
|
1193
|
-
categories = [
|
|
1194
|
-
{"Griptape Nodes Sandbox": sandbox_category},
|
|
1195
|
-
]
|
|
1714
|
+
# Use the existing schema but replace nodes with actual discovered ones
|
|
1196
1715
|
library_data = LibrarySchema(
|
|
1197
|
-
name=
|
|
1198
|
-
library_schema_version=
|
|
1199
|
-
metadata=
|
|
1200
|
-
categories=categories,
|
|
1201
|
-
nodes=
|
|
1716
|
+
name=library_schema.name,
|
|
1717
|
+
library_schema_version=library_schema.library_schema_version,
|
|
1718
|
+
metadata=library_schema.metadata,
|
|
1719
|
+
categories=library_schema.categories,
|
|
1720
|
+
nodes=actual_node_definitions,
|
|
1202
1721
|
)
|
|
1203
1722
|
|
|
1204
1723
|
# Register the library.
|
|
@@ -1215,22 +1734,24 @@ class LibraryManager:
|
|
|
1215
1734
|
self._library_file_path_to_info[sandbox_library_dir_as_posix] = LibraryManager.LibraryInfo(
|
|
1216
1735
|
library_path=sandbox_library_dir_as_posix,
|
|
1217
1736
|
library_name=library_data.name,
|
|
1218
|
-
library_version=
|
|
1737
|
+
library_version=library_data.metadata.library_version,
|
|
1219
1738
|
status=LibraryManager.LibraryStatus.UNUSABLE,
|
|
1220
|
-
problems=[
|
|
1739
|
+
problems=[
|
|
1740
|
+
"Failed because a library with this name was already registered. Check the Settings to ensure duplicate libraries are not being loaded."
|
|
1741
|
+
],
|
|
1221
1742
|
)
|
|
1222
1743
|
|
|
1223
1744
|
details = f"Attempted to load Library JSON file from '{sandbox_library_dir}'. Failed because a Library '{library_data.name}' already exists. Error: {err}."
|
|
1224
1745
|
logger.error(details)
|
|
1225
1746
|
return
|
|
1226
1747
|
|
|
1227
|
-
#
|
|
1748
|
+
# Load nodes into the library
|
|
1228
1749
|
library_load_results = self._attempt_load_nodes_from_library(
|
|
1229
1750
|
library_data=library_data,
|
|
1230
1751
|
library=library,
|
|
1231
|
-
base_dir=
|
|
1752
|
+
base_dir=sandbox_library_dir,
|
|
1232
1753
|
library_file_path=sandbox_library_dir_as_posix,
|
|
1233
|
-
library_version=
|
|
1754
|
+
library_version=library_data.metadata.library_version,
|
|
1234
1755
|
problems=problems,
|
|
1235
1756
|
)
|
|
1236
1757
|
self._library_file_path_to_info[sandbox_library_dir_as_posix] = library_load_results
|