griptape-nodes 0.70.1__py3-none-any.whl → 0.72.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/api_client/client.py +8 -5
- griptape_nodes/app/app.py +4 -0
- griptape_nodes/bootstrap/utils/python_subprocess_executor.py +48 -9
- griptape_nodes/bootstrap/utils/subprocess_websocket_base.py +88 -0
- griptape_nodes/bootstrap/utils/subprocess_websocket_listener.py +126 -0
- griptape_nodes/bootstrap/utils/subprocess_websocket_sender.py +121 -0
- griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +17 -170
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +10 -1
- griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +13 -117
- griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +4 -0
- griptape_nodes/bootstrap/workflow_publishers/local_session_workflow_publisher.py +206 -0
- griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +22 -3
- griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +49 -25
- griptape_nodes/common/node_executor.py +61 -14
- griptape_nodes/drivers/image_metadata/__init__.py +21 -0
- griptape_nodes/drivers/image_metadata/base_image_metadata_driver.py +63 -0
- griptape_nodes/drivers/image_metadata/exif_metadata_driver.py +218 -0
- griptape_nodes/drivers/image_metadata/image_metadata_driver_registry.py +55 -0
- griptape_nodes/drivers/image_metadata/png_metadata_driver.py +71 -0
- griptape_nodes/drivers/storage/base_storage_driver.py +32 -0
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +384 -10
- griptape_nodes/drivers/storage/local_storage_driver.py +65 -4
- griptape_nodes/drivers/thread_storage/local_thread_storage_driver.py +1 -0
- griptape_nodes/exe_types/base_iterative_nodes.py +1 -1
- griptape_nodes/exe_types/node_groups/base_node_group.py +3 -0
- griptape_nodes/exe_types/node_groups/subflow_node_group.py +18 -0
- griptape_nodes/exe_types/node_types.py +13 -0
- griptape_nodes/exe_types/param_components/log_parameter.py +4 -4
- griptape_nodes/exe_types/param_components/subflow_execution_component.py +329 -0
- griptape_nodes/exe_types/param_types/parameter_audio.py +17 -2
- griptape_nodes/exe_types/param_types/parameter_float.py +4 -4
- griptape_nodes/exe_types/param_types/parameter_image.py +14 -1
- griptape_nodes/exe_types/param_types/parameter_int.py +4 -4
- griptape_nodes/exe_types/param_types/parameter_number.py +12 -14
- griptape_nodes/exe_types/param_types/parameter_three_d.py +14 -1
- griptape_nodes/exe_types/param_types/parameter_video.py +17 -2
- griptape_nodes/node_library/workflow_registry.py +5 -8
- griptape_nodes/retained_mode/events/app_events.py +1 -0
- griptape_nodes/retained_mode/events/base_events.py +42 -26
- griptape_nodes/retained_mode/events/flow_events.py +67 -0
- griptape_nodes/retained_mode/events/library_events.py +1 -1
- griptape_nodes/retained_mode/events/node_events.py +1 -0
- griptape_nodes/retained_mode/events/os_events.py +22 -0
- griptape_nodes/retained_mode/events/static_file_events.py +28 -4
- griptape_nodes/retained_mode/managers/flow_manager.py +134 -0
- griptape_nodes/retained_mode/managers/image_metadata_injector.py +339 -0
- griptape_nodes/retained_mode/managers/library_manager.py +71 -41
- griptape_nodes/retained_mode/managers/model_manager.py +1 -0
- griptape_nodes/retained_mode/managers/node_manager.py +8 -5
- griptape_nodes/retained_mode/managers/os_manager.py +270 -33
- griptape_nodes/retained_mode/managers/project_manager.py +3 -7
- griptape_nodes/retained_mode/managers/session_manager.py +1 -0
- griptape_nodes/retained_mode/managers/settings.py +5 -0
- griptape_nodes/retained_mode/managers/static_files_manager.py +83 -17
- griptape_nodes/retained_mode/managers/workflow_manager.py +71 -41
- griptape_nodes/servers/static.py +31 -0
- griptape_nodes/utils/__init__.py +9 -1
- griptape_nodes/utils/artifact_normalization.py +245 -0
- griptape_nodes/utils/file_utils.py +13 -13
- griptape_nodes/utils/http_file_patch.py +613 -0
- griptape_nodes/utils/image_preview.py +27 -0
- griptape_nodes/utils/path_utils.py +58 -0
- griptape_nodes/utils/url_utils.py +106 -0
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/METADATA +2 -1
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/RECORD +67 -52
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.70.1.dist-info → griptape_nodes-0.72.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
import logging
|
|
5
|
+
import pickle
|
|
4
6
|
from enum import StrEnum
|
|
7
|
+
from io import BytesIO
|
|
8
|
+
from pathlib import Path
|
|
5
9
|
from queue import Queue
|
|
6
10
|
from typing import TYPE_CHECKING, Any, NamedTuple, cast
|
|
7
11
|
from uuid import uuid4
|
|
8
12
|
|
|
13
|
+
import httpx
|
|
14
|
+
from PIL import Image
|
|
15
|
+
|
|
9
16
|
from griptape_nodes.common.node_executor import NodeExecutor
|
|
10
17
|
from griptape_nodes.exe_types.base_iterative_nodes import BaseIterativeStartNode
|
|
11
18
|
from griptape_nodes.exe_types.connections import Connections
|
|
@@ -92,6 +99,9 @@ from griptape_nodes.retained_mode.events.flow_events import (
|
|
|
92
99
|
DeserializeFlowFromCommandsRequest,
|
|
93
100
|
DeserializeFlowFromCommandsResultFailure,
|
|
94
101
|
DeserializeFlowFromCommandsResultSuccess,
|
|
102
|
+
ExtractFlowCommandsFromImageMetadataRequest,
|
|
103
|
+
ExtractFlowCommandsFromImageMetadataResultFailure,
|
|
104
|
+
ExtractFlowCommandsFromImageMetadataResultSuccess,
|
|
95
105
|
GetFlowDetailsRequest,
|
|
96
106
|
GetFlowDetailsResultFailure,
|
|
97
107
|
GetFlowDetailsResultSuccess,
|
|
@@ -148,6 +158,7 @@ from griptape_nodes.retained_mode.events.workflow_events import (
|
|
|
148
158
|
ImportWorkflowAsReferencedSubFlowResultSuccess,
|
|
149
159
|
)
|
|
150
160
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
161
|
+
from griptape_nodes.retained_mode.managers.image_metadata_injector import FLOW_COMMANDS_KEY
|
|
151
162
|
|
|
152
163
|
if TYPE_CHECKING:
|
|
153
164
|
from griptape_nodes.retained_mode.events.base_events import ResultPayload
|
|
@@ -268,6 +279,9 @@ class FlowManager:
|
|
|
268
279
|
event_manager.assign_manager_to_request_type(
|
|
269
280
|
DeserializeFlowFromCommandsRequest, self.on_deserialize_flow_from_commands
|
|
270
281
|
)
|
|
282
|
+
event_manager.assign_manager_to_request_type(
|
|
283
|
+
ExtractFlowCommandsFromImageMetadataRequest, self.on_extract_flow_commands_from_image_metadata
|
|
284
|
+
)
|
|
271
285
|
event_manager.assign_manager_to_request_type(
|
|
272
286
|
PackageNodesAsSerializedFlowRequest, self.on_package_nodes_as_serialized_flow_request
|
|
273
287
|
)
|
|
@@ -3601,6 +3615,126 @@ class FlowManager:
|
|
|
3601
3615
|
ExecutionGriptapeNodeEvent(wrapped_event=ExecutionEvent(payload=InvolvedNodesEvent(involved_nodes=[])))
|
|
3602
3616
|
)
|
|
3603
3617
|
|
|
3618
|
+
def on_extract_flow_commands_from_image_metadata( # noqa: PLR0911, C901
|
|
3619
|
+
self, request: ExtractFlowCommandsFromImageMetadataRequest
|
|
3620
|
+
) -> ResultPayload:
|
|
3621
|
+
"""Extract flow commands from PNG image metadata.
|
|
3622
|
+
|
|
3623
|
+
Args:
|
|
3624
|
+
request: ExtractFlowCommandsFromImageMetadataRequest with file path or URL
|
|
3625
|
+
|
|
3626
|
+
Returns:
|
|
3627
|
+
ExtractFlowCommandsFromImageMetadataResultSuccess if successful
|
|
3628
|
+
ExtractFlowCommandsFromImageMetadataResultFailure if any step fails
|
|
3629
|
+
"""
|
|
3630
|
+
file_url_or_path = request.file_url_or_path
|
|
3631
|
+
|
|
3632
|
+
# Check if input is a URL or file path
|
|
3633
|
+
is_url = file_url_or_path.startswith(("http://", "https://"))
|
|
3634
|
+
|
|
3635
|
+
if is_url:
|
|
3636
|
+
# Handle URL: download the image
|
|
3637
|
+
try:
|
|
3638
|
+
response = httpx.get(file_url_or_path, timeout=30.0)
|
|
3639
|
+
response.raise_for_status()
|
|
3640
|
+
pil_image = Image.open(BytesIO(response.content))
|
|
3641
|
+
except Exception as e:
|
|
3642
|
+
return ExtractFlowCommandsFromImageMetadataResultFailure(
|
|
3643
|
+
result_details=f"Failed to download or open image from URL: {e}",
|
|
3644
|
+
file_path=file_url_or_path,
|
|
3645
|
+
)
|
|
3646
|
+
else:
|
|
3647
|
+
# Handle file path: validate and open
|
|
3648
|
+
path_obj = Path(file_url_or_path)
|
|
3649
|
+
if not path_obj.exists():
|
|
3650
|
+
return ExtractFlowCommandsFromImageMetadataResultFailure(
|
|
3651
|
+
result_details=f"File not found: {file_url_or_path}",
|
|
3652
|
+
file_path=file_url_or_path,
|
|
3653
|
+
)
|
|
3654
|
+
|
|
3655
|
+
if not path_obj.is_file():
|
|
3656
|
+
return ExtractFlowCommandsFromImageMetadataResultFailure(
|
|
3657
|
+
result_details=f"Path is not a file: {file_url_or_path}",
|
|
3658
|
+
file_path=file_url_or_path,
|
|
3659
|
+
)
|
|
3660
|
+
|
|
3661
|
+
# Read PNG image and extract metadata
|
|
3662
|
+
try:
|
|
3663
|
+
pil_image = Image.open(file_url_or_path)
|
|
3664
|
+
except Exception as e:
|
|
3665
|
+
return ExtractFlowCommandsFromImageMetadataResultFailure(
|
|
3666
|
+
result_details=f"Failed to open image file: {e}",
|
|
3667
|
+
file_path=file_url_or_path,
|
|
3668
|
+
)
|
|
3669
|
+
|
|
3670
|
+
# Validation: Check if image has metadata
|
|
3671
|
+
if not hasattr(pil_image, "info") or not pil_image.info:
|
|
3672
|
+
return ExtractFlowCommandsFromImageMetadataResultFailure(
|
|
3673
|
+
result_details=f"Image has no metadata: {file_url_or_path}",
|
|
3674
|
+
file_path=file_url_or_path,
|
|
3675
|
+
)
|
|
3676
|
+
|
|
3677
|
+
metadata = pil_image.info
|
|
3678
|
+
metadata_keys = [str(key) for key in metadata]
|
|
3679
|
+
|
|
3680
|
+
# Validation: Check if flow commands metadata exists
|
|
3681
|
+
if FLOW_COMMANDS_KEY not in metadata:
|
|
3682
|
+
return ExtractFlowCommandsFromImageMetadataResultFailure(
|
|
3683
|
+
result_details=f"No flow commands metadata found in image. Available keys: {metadata_keys}",
|
|
3684
|
+
file_path=file_url_or_path,
|
|
3685
|
+
)
|
|
3686
|
+
|
|
3687
|
+
encoded_flow_commands = metadata[FLOW_COMMANDS_KEY]
|
|
3688
|
+
|
|
3689
|
+
# Decode base64
|
|
3690
|
+
try:
|
|
3691
|
+
pickled_data = base64.b64decode(encoded_flow_commands)
|
|
3692
|
+
except Exception as e:
|
|
3693
|
+
return ExtractFlowCommandsFromImageMetadataResultFailure(
|
|
3694
|
+
result_details=f"Failed to decode base64 flow commands: {e}",
|
|
3695
|
+
file_path=file_url_or_path,
|
|
3696
|
+
)
|
|
3697
|
+
|
|
3698
|
+
# Unpickle SerializedFlowCommands
|
|
3699
|
+
try:
|
|
3700
|
+
# Pickle is safe here: we're deserializing workflow data from images saved by this application
|
|
3701
|
+
# Converting to JSON would require significant serialization infrastructure for SerializedFlowCommands
|
|
3702
|
+
serialized_flow_commands = pickle.loads(pickled_data) # noqa: S301
|
|
3703
|
+
except Exception as e:
|
|
3704
|
+
return ExtractFlowCommandsFromImageMetadataResultFailure(
|
|
3705
|
+
result_details=f"Failed to unpickle flow commands: {e}",
|
|
3706
|
+
file_path=file_url_or_path,
|
|
3707
|
+
)
|
|
3708
|
+
|
|
3709
|
+
# Check if we should also deserialize the flow
|
|
3710
|
+
if request.deserialize:
|
|
3711
|
+
# Deserialize the flow
|
|
3712
|
+
deserialize_request = DeserializeFlowFromCommandsRequest(serialized_flow_commands=serialized_flow_commands)
|
|
3713
|
+
deserialize_result = GriptapeNodes.handle_request(deserialize_request)
|
|
3714
|
+
|
|
3715
|
+
# Validation: Check if deserialization succeeded
|
|
3716
|
+
if not isinstance(deserialize_result, DeserializeFlowFromCommandsResultSuccess):
|
|
3717
|
+
return ExtractFlowCommandsFromImageMetadataResultFailure(
|
|
3718
|
+
result_details=f"Failed to deserialize flow from image metadata: {deserialize_result.result_details}",
|
|
3719
|
+
file_path=file_url_or_path,
|
|
3720
|
+
)
|
|
3721
|
+
|
|
3722
|
+
# Success path: Return with deserialization info
|
|
3723
|
+
return ExtractFlowCommandsFromImageMetadataResultSuccess(
|
|
3724
|
+
result_details=f"Successfully extracted and deserialized flow '{deserialize_result.flow_name}' from image metadata",
|
|
3725
|
+
serialized_flow_commands=serialized_flow_commands,
|
|
3726
|
+
flow_name=deserialize_result.flow_name,
|
|
3727
|
+
node_name_mappings=deserialize_result.node_name_mappings,
|
|
3728
|
+
altered_workflow_state=True,
|
|
3729
|
+
)
|
|
3730
|
+
|
|
3731
|
+
# Success path: Return the extracted commands for user inspection (no deserialization)
|
|
3732
|
+
return ExtractFlowCommandsFromImageMetadataResultSuccess(
|
|
3733
|
+
result_details="Successfully extracted flow commands from image metadata",
|
|
3734
|
+
serialized_flow_commands=serialized_flow_commands,
|
|
3735
|
+
altered_workflow_state=False,
|
|
3736
|
+
)
|
|
3737
|
+
|
|
3604
3738
|
def check_for_existing_running_flow(self) -> bool:
|
|
3605
3739
|
if self._global_control_flow_machine is None:
|
|
3606
3740
|
return False
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Automatic workflow metadata injection for images saved through StaticFilesManager.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to automatically inject workflow metadata into
|
|
4
|
+
images when they are saved. Metadata format depends on image type:
|
|
5
|
+
- PNG: Stored in PNG text chunks
|
|
6
|
+
- JPEG/TIFF/MPO: Stored as JSON in EXIF UserComment field
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import logging
|
|
11
|
+
import pickle
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from io import BytesIO
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from PIL import Image
|
|
18
|
+
|
|
19
|
+
from griptape_nodes.drivers.image_metadata.image_metadata_driver_registry import (
|
|
20
|
+
ImageMetadataDriverRegistry,
|
|
21
|
+
)
|
|
22
|
+
from griptape_nodes.exe_types.core_types import ParameterMode
|
|
23
|
+
from griptape_nodes.exe_types.node_types import BaseNode
|
|
24
|
+
from griptape_nodes.exe_types.type_validator import TypeValidator
|
|
25
|
+
from griptape_nodes.node_library.workflow_registry import WorkflowRegistry
|
|
26
|
+
from griptape_nodes.retained_mode.events.flow_events import (
|
|
27
|
+
SerializeFlowToCommandsRequest,
|
|
28
|
+
SerializeFlowToCommandsResultSuccess,
|
|
29
|
+
)
|
|
30
|
+
from griptape_nodes.retained_mode.events.node_events import (
|
|
31
|
+
SerializeNodeToCommandsRequest,
|
|
32
|
+
SerializeNodeToCommandsResultSuccess,
|
|
33
|
+
)
|
|
34
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger("griptape_nodes")
|
|
37
|
+
|
|
38
|
+
# Metadata namespace prefix for all auto-injected fields
|
|
39
|
+
METADATA_NAMESPACE = "gtn_"
|
|
40
|
+
|
|
41
|
+
# Metadata key for storing flow commands
|
|
42
|
+
FLOW_COMMANDS_KEY = f"{METADATA_NAMESPACE}flow_commands"
|
|
43
|
+
|
|
44
|
+
# Recognized image formats for file extension mapping (not all support metadata injection)
|
|
45
|
+
SUPPORTED_FORMATS = {
|
|
46
|
+
".jpg": "JPEG",
|
|
47
|
+
".jpeg": "JPEG",
|
|
48
|
+
".png": "PNG",
|
|
49
|
+
".tiff": "TIFF",
|
|
50
|
+
".tif": "TIFF",
|
|
51
|
+
".mpo": "MPO",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_image_format_from_filename(filename: str) -> str | None:
|
|
56
|
+
"""Extract image format from filename extension.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
filename: Name of the file including extension
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Image format string (e.g., "JPEG", "PNG") or None if not a supported image format
|
|
63
|
+
"""
|
|
64
|
+
ext = Path(filename).suffix.lower()
|
|
65
|
+
return SUPPORTED_FORMATS.get(ext)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def supports_metadata(format_str: str | None) -> bool:
|
|
69
|
+
"""Check if image format supports automatic metadata injection.
|
|
70
|
+
|
|
71
|
+
Currently limited to PNG to avoid EXIF size limits on workflow metadata.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
format_str: Image format string (e.g., "PNG", "JPEG")
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
True only for PNG format
|
|
78
|
+
"""
|
|
79
|
+
return format_str == "PNG"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _serialize_node(node_name: str) -> str | None:
|
|
83
|
+
"""Serialize a specific node to JSON commands.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
node_name: Name of the node to serialize
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
JSON string of serialized node commands, or None if serialization fails
|
|
90
|
+
"""
|
|
91
|
+
serialize_request = SerializeNodeToCommandsRequest(
|
|
92
|
+
node_name=node_name,
|
|
93
|
+
)
|
|
94
|
+
serialize_result = GriptapeNodes.handle_request(serialize_request)
|
|
95
|
+
|
|
96
|
+
if isinstance(serialize_result, SerializeNodeToCommandsResultSuccess):
|
|
97
|
+
# Convert to dict and then to JSON string
|
|
98
|
+
return serialize_result.to_json()
|
|
99
|
+
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _serialize_flow(flow_name: str | None = None) -> str | None:
|
|
104
|
+
"""Serialize a flow to pickle + base64 encoded commands.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
flow_name: Name of the flow to serialize (None for current context flow)
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Base64-encoded pickle string of serialized flow commands, or None if serialization fails
|
|
111
|
+
"""
|
|
112
|
+
# Validation: Check if we have a flow context
|
|
113
|
+
if flow_name is None and not GriptapeNodes.ContextManager().has_current_flow():
|
|
114
|
+
logger.warning("Cannot serialize flow: no current flow context available")
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
# Create serialize request
|
|
118
|
+
serialize_request = SerializeFlowToCommandsRequest(
|
|
119
|
+
flow_name=flow_name,
|
|
120
|
+
include_create_flow_command=False,
|
|
121
|
+
)
|
|
122
|
+
serialize_result = GriptapeNodes.handle_request(serialize_request)
|
|
123
|
+
|
|
124
|
+
# Validation: Check if serialization succeeded
|
|
125
|
+
if not isinstance(serialize_result, SerializeFlowToCommandsResultSuccess):
|
|
126
|
+
logger.warning("Failed to serialize flow '%s' to commands", flow_name or "current")
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
# Success path: Serialize using pickle + base64
|
|
130
|
+
try:
|
|
131
|
+
serialized_flow_commands = serialize_result.serialized_flow_commands
|
|
132
|
+
# Pickle is safe here: serializing workflow data for metadata injection into saved images
|
|
133
|
+
# The data will only be deserialized by this same application
|
|
134
|
+
pickled_data = pickle.dumps(serialized_flow_commands)
|
|
135
|
+
encoded_data = base64.b64encode(pickled_data).decode("ascii")
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.warning("Failed to pickle/encode flow '%s': %s", flow_name or "current", e)
|
|
138
|
+
return None
|
|
139
|
+
else:
|
|
140
|
+
return encoded_data
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _collect_parameter_values(node_name: str) -> dict[str, Any] | None:
|
|
144
|
+
"""Collect current parameter values from a node's INPUT and PROPERTY parameters.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
node_name: Name of the node to collect parameters from
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Dictionary of parameter names to serialized values, or None if collection fails
|
|
151
|
+
"""
|
|
152
|
+
# Failure case: Attempt to get node object
|
|
153
|
+
obj_mgr = GriptapeNodes.ObjectManager()
|
|
154
|
+
try:
|
|
155
|
+
node = obj_mgr.attempt_get_object_by_name_as_type(node_name, BaseNode)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
logger.warning("Failed to get node '%s' for parameter collection: %s", node_name, e)
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
if node is None:
|
|
161
|
+
logger.warning("Node '%s' not found for parameter collection", node_name)
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
# Get all parameters from node
|
|
165
|
+
all_parameters = node.parameters
|
|
166
|
+
|
|
167
|
+
# Filter to INPUT and PROPERTY mode parameters only
|
|
168
|
+
eligible_parameters = [
|
|
169
|
+
param
|
|
170
|
+
for param in all_parameters
|
|
171
|
+
if ParameterMode.INPUT in param.allowed_modes or ParameterMode.PROPERTY in param.allowed_modes
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
# Collect and serialize parameter values
|
|
175
|
+
parameter_values = {}
|
|
176
|
+
|
|
177
|
+
for param in eligible_parameters:
|
|
178
|
+
# Get current value
|
|
179
|
+
value = node.get_parameter_value(param.name)
|
|
180
|
+
|
|
181
|
+
# Skip None values (not set)
|
|
182
|
+
if value is None:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
# Serialize value with error handling
|
|
186
|
+
try:
|
|
187
|
+
serialized_value = TypeValidator.safe_serialize(value)
|
|
188
|
+
parameter_values[param.name] = serialized_value
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.warning("Failed to serialize parameter '%s' on node '%s': %s", param.name, node_name, e)
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
# Success path: return collected values (may be empty dict)
|
|
194
|
+
return parameter_values
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _collect_workflow_details(workflow_name: str, metadata: dict[str, str]) -> None:
|
|
198
|
+
"""Collect workflow details from registry and add to metadata dict.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
workflow_name: Name of the workflow
|
|
202
|
+
metadata: Dictionary to populate with workflow metadata (modified in-place)
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
workflow = WorkflowRegistry.get_workflow_by_name(workflow_name)
|
|
206
|
+
|
|
207
|
+
if workflow.metadata.creation_date:
|
|
208
|
+
metadata[f"{METADATA_NAMESPACE}workflow_created"] = workflow.metadata.creation_date.isoformat()
|
|
209
|
+
|
|
210
|
+
if workflow.metadata.last_modified_date:
|
|
211
|
+
metadata[f"{METADATA_NAMESPACE}workflow_modified"] = workflow.metadata.last_modified_date.isoformat()
|
|
212
|
+
|
|
213
|
+
if workflow.metadata.engine_version_created_with:
|
|
214
|
+
metadata[f"{METADATA_NAMESPACE}engine_version"] = workflow.metadata.engine_version_created_with
|
|
215
|
+
|
|
216
|
+
if workflow.metadata.description:
|
|
217
|
+
metadata[f"{METADATA_NAMESPACE}workflow_description"] = workflow.metadata.description
|
|
218
|
+
except Exception: # noqa: S110
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def collect_workflow_metadata() -> dict[str, str]:
|
|
223
|
+
"""Collect available workflow metadata from current execution context.
|
|
224
|
+
|
|
225
|
+
Gathers metadata from GriptapeNodes ContextManager and WorkflowRegistry.
|
|
226
|
+
All keys are prefixed with METADATA_NAMESPACE to avoid conflicts.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Dictionary of metadata key-value pairs, may be empty if no context available
|
|
230
|
+
"""
|
|
231
|
+
metadata = {}
|
|
232
|
+
|
|
233
|
+
# Add save timestamp (always available)
|
|
234
|
+
metadata[f"{METADATA_NAMESPACE}saved_at"] = datetime.now(UTC).isoformat()
|
|
235
|
+
|
|
236
|
+
# Get context manager
|
|
237
|
+
context_manager = GriptapeNodes.ContextManager()
|
|
238
|
+
|
|
239
|
+
# Check workflow context
|
|
240
|
+
if not context_manager.has_current_workflow():
|
|
241
|
+
return metadata
|
|
242
|
+
|
|
243
|
+
# Get workflow name
|
|
244
|
+
try:
|
|
245
|
+
workflow_name = context_manager.get_current_workflow_name()
|
|
246
|
+
metadata[f"{METADATA_NAMESPACE}workflow_name"] = workflow_name
|
|
247
|
+
_collect_workflow_details(workflow_name, metadata)
|
|
248
|
+
except Exception: # noqa: S110
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
# Get flow context and resolving nodes
|
|
252
|
+
if context_manager.has_current_flow():
|
|
253
|
+
try:
|
|
254
|
+
flow = context_manager.get_current_flow()
|
|
255
|
+
metadata[f"{METADATA_NAMESPACE}flow_name"] = flow.name
|
|
256
|
+
|
|
257
|
+
# Get resolving nodes (currently running nodes) from flow_state
|
|
258
|
+
flow_manager = GriptapeNodes.FlowManager()
|
|
259
|
+
_, resolving_nodes, _ = flow_manager.flow_state(flow)
|
|
260
|
+
|
|
261
|
+
if resolving_nodes:
|
|
262
|
+
# Store node name(s) - if multiple, join with comma
|
|
263
|
+
metadata[f"{METADATA_NAMESPACE}node_name"] = ", ".join(resolving_nodes)
|
|
264
|
+
|
|
265
|
+
# Serialize the entire current flow to commands
|
|
266
|
+
# This captures all nodes, connections, and parameter values in the flow
|
|
267
|
+
flow_commands = _serialize_flow()
|
|
268
|
+
if flow_commands:
|
|
269
|
+
metadata[FLOW_COMMANDS_KEY] = flow_commands
|
|
270
|
+
|
|
271
|
+
if resolving_nodes:
|
|
272
|
+
# Collect parameter values from the first resolving node
|
|
273
|
+
parameter_values = _collect_parameter_values(resolving_nodes[0])
|
|
274
|
+
if parameter_values:
|
|
275
|
+
# Store each parameter as its own metadata key
|
|
276
|
+
for param_name, param_value in parameter_values.items():
|
|
277
|
+
metadata[f"{METADATA_NAMESPACE}param_{param_name}"] = str(param_value)
|
|
278
|
+
except Exception:
|
|
279
|
+
logger.exception("Failed to collect flow/node metadata")
|
|
280
|
+
|
|
281
|
+
return metadata
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def inject_workflow_metadata_if_image(data: bytes, file_name: str) -> bytes: # noqa: PLR0911
|
|
285
|
+
"""Inject workflow metadata into image if format supports it.
|
|
286
|
+
|
|
287
|
+
Main entry point for automatic metadata injection. Detects image format,
|
|
288
|
+
collects workflow metadata, and delegates to appropriate driver.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
data: Raw image bytes
|
|
292
|
+
file_name: Filename including extension
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Image bytes with metadata injected, or original bytes if:
|
|
296
|
+
- Format doesn't support metadata
|
|
297
|
+
- No workflow context available
|
|
298
|
+
- Image loading/processing fails
|
|
299
|
+
"""
|
|
300
|
+
# Validation: Check format
|
|
301
|
+
format_str = get_image_format_from_filename(file_name)
|
|
302
|
+
if not supports_metadata(format_str):
|
|
303
|
+
return data
|
|
304
|
+
|
|
305
|
+
# Validation: Check if we have data
|
|
306
|
+
if not data:
|
|
307
|
+
logger.warning("Cannot inject metadata: empty data")
|
|
308
|
+
return data
|
|
309
|
+
|
|
310
|
+
# Collect metadata
|
|
311
|
+
metadata = collect_workflow_metadata()
|
|
312
|
+
if not metadata:
|
|
313
|
+
# No context available, nothing to inject
|
|
314
|
+
return data
|
|
315
|
+
|
|
316
|
+
# Load PIL image
|
|
317
|
+
try:
|
|
318
|
+
pil_image = Image.open(BytesIO(data))
|
|
319
|
+
except Exception as e:
|
|
320
|
+
logger.warning("Failed to load image for metadata injection: %s", e)
|
|
321
|
+
return data
|
|
322
|
+
|
|
323
|
+
# Verify format matches
|
|
324
|
+
if pil_image.format is None:
|
|
325
|
+
logger.warning("Could not detect image format from data")
|
|
326
|
+
return data
|
|
327
|
+
|
|
328
|
+
# Get driver for this format
|
|
329
|
+
driver = ImageMetadataDriverRegistry.get_driver_for_format(pil_image.format)
|
|
330
|
+
if driver is None:
|
|
331
|
+
logger.warning("No metadata driver found for format: %s", pil_image.format)
|
|
332
|
+
return data
|
|
333
|
+
|
|
334
|
+
# Inject metadata using driver
|
|
335
|
+
try:
|
|
336
|
+
return driver.inject_metadata(pil_image, metadata)
|
|
337
|
+
except Exception as e:
|
|
338
|
+
logger.warning("Failed to inject metadata into %s: %s", file_name, e)
|
|
339
|
+
return data
|