griptape-nodes 0.52.1__py3-none-any.whl → 0.54.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 +8 -942
- griptape_nodes/__main__.py +6 -0
- griptape_nodes/app/app.py +48 -86
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +35 -5
- griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +15 -1
- griptape_nodes/cli/__init__.py +1 -0
- griptape_nodes/cli/commands/__init__.py +1 -0
- griptape_nodes/cli/commands/config.py +74 -0
- griptape_nodes/cli/commands/engine.py +80 -0
- griptape_nodes/cli/commands/init.py +550 -0
- griptape_nodes/cli/commands/libraries.py +96 -0
- griptape_nodes/cli/commands/models.py +504 -0
- griptape_nodes/cli/commands/self.py +120 -0
- griptape_nodes/cli/main.py +56 -0
- griptape_nodes/cli/shared.py +75 -0
- griptape_nodes/common/__init__.py +1 -0
- griptape_nodes/common/directed_graph.py +71 -0
- griptape_nodes/drivers/storage/base_storage_driver.py +40 -20
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +24 -29
- griptape_nodes/drivers/storage/local_storage_driver.py +23 -14
- griptape_nodes/exe_types/core_types.py +60 -2
- griptape_nodes/exe_types/node_types.py +257 -38
- griptape_nodes/exe_types/param_components/__init__.py +1 -0
- griptape_nodes/exe_types/param_components/execution_status_component.py +138 -0
- griptape_nodes/machines/control_flow.py +195 -94
- griptape_nodes/machines/dag_builder.py +207 -0
- griptape_nodes/machines/fsm.py +10 -1
- griptape_nodes/machines/parallel_resolution.py +558 -0
- griptape_nodes/machines/{node_resolution.py → sequential_resolution.py} +30 -57
- griptape_nodes/node_library/library_registry.py +34 -1
- griptape_nodes/retained_mode/events/app_events.py +5 -1
- griptape_nodes/retained_mode/events/base_events.py +9 -9
- griptape_nodes/retained_mode/events/config_events.py +30 -0
- griptape_nodes/retained_mode/events/execution_events.py +2 -2
- griptape_nodes/retained_mode/events/model_events.py +296 -0
- griptape_nodes/retained_mode/events/node_events.py +4 -3
- griptape_nodes/retained_mode/griptape_nodes.py +34 -12
- griptape_nodes/retained_mode/managers/agent_manager.py +23 -5
- griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +3 -1
- griptape_nodes/retained_mode/managers/config_manager.py +44 -3
- griptape_nodes/retained_mode/managers/context_manager.py +6 -5
- griptape_nodes/retained_mode/managers/event_manager.py +8 -2
- griptape_nodes/retained_mode/managers/flow_manager.py +150 -206
- griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +1 -1
- griptape_nodes/retained_mode/managers/library_manager.py +35 -25
- griptape_nodes/retained_mode/managers/model_manager.py +1107 -0
- griptape_nodes/retained_mode/managers/node_manager.py +102 -220
- griptape_nodes/retained_mode/managers/object_manager.py +11 -5
- griptape_nodes/retained_mode/managers/os_manager.py +28 -13
- griptape_nodes/retained_mode/managers/secrets_manager.py +8 -4
- griptape_nodes/retained_mode/managers/settings.py +116 -7
- griptape_nodes/retained_mode/managers/static_files_manager.py +85 -12
- griptape_nodes/retained_mode/managers/sync_manager.py +17 -9
- griptape_nodes/retained_mode/managers/workflow_manager.py +186 -192
- griptape_nodes/retained_mode/retained_mode.py +19 -0
- griptape_nodes/servers/__init__.py +1 -0
- griptape_nodes/{mcp_server/server.py → servers/mcp.py} +1 -1
- griptape_nodes/{app/api.py → servers/static.py} +43 -40
- griptape_nodes/traits/add_param_button.py +1 -1
- griptape_nodes/traits/button.py +334 -6
- griptape_nodes/traits/color_picker.py +66 -0
- griptape_nodes/traits/multi_options.py +188 -0
- griptape_nodes/traits/numbers_selector.py +77 -0
- griptape_nodes/traits/options.py +93 -2
- griptape_nodes/traits/traits.json +4 -0
- griptape_nodes/utils/async_utils.py +31 -0
- {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/METADATA +4 -1
- {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/RECORD +71 -48
- {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/WHEEL +1 -1
- /griptape_nodes/{mcp_server → servers}/ws_request_manager.py +0 -0
- {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from griptape_nodes.exe_types.core_types import Parameter, Trait
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(eq=False)
|
|
9
|
+
class MultiOptions(Trait):
|
|
10
|
+
# SERIALIZATION BUG FIX EXPLANATION:
|
|
11
|
+
#
|
|
12
|
+
# PROBLEM: Similar to Options trait, MultiOptions had a potential serialization bug
|
|
13
|
+
# where dynamically populated multi-options lists would work correctly during runtime
|
|
14
|
+
# but could revert after save/reload cycles. This happens because:
|
|
15
|
+
# 1. trait.choices was the "source of truth" during runtime
|
|
16
|
+
# 2. ui_options["multi_options"] was populated from trait.choices
|
|
17
|
+
# 3. Only ui_options gets serialized/deserialized (not trait fields)
|
|
18
|
+
# 4. After reload, trait.choices was stale but ui_options had correct data
|
|
19
|
+
# 5. Converters used stale trait.choices, causing validation issues
|
|
20
|
+
#
|
|
21
|
+
# SOLUTION: Make ui_options the primary source of truth, with _choices as fallback
|
|
22
|
+
# 1. choices property reads from ui_options["multi_options"] when available
|
|
23
|
+
# 2. choices setter writes to BOTH _choices and ui_options (dual sync)
|
|
24
|
+
# 3. This ensures serialized ui_options data is used after deserialization
|
|
25
|
+
# 4. _choices provides safety fallback if ui_options is missing/corrupted
|
|
26
|
+
|
|
27
|
+
_choices: list = field(default_factory=lambda: ["choice 1", "choice 2", "choice 3"])
|
|
28
|
+
element_id: str = field(default_factory=lambda: "MultiOptions")
|
|
29
|
+
placeholder: str = field(default="Select options...")
|
|
30
|
+
max_selected_display: int = field(default=3)
|
|
31
|
+
show_search: bool = field(default=True)
|
|
32
|
+
icon_size: str = field(default="small")
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
choices: list | None = None,
|
|
38
|
+
placeholder: str = "Select options...",
|
|
39
|
+
max_selected_display: int = 3,
|
|
40
|
+
show_search: bool = True,
|
|
41
|
+
icon_size: str = "small",
|
|
42
|
+
) -> None:
|
|
43
|
+
super().__init__()
|
|
44
|
+
# Set choices through property to ensure dual sync from the start
|
|
45
|
+
if choices is not None:
|
|
46
|
+
self.choices = choices
|
|
47
|
+
|
|
48
|
+
self.placeholder = placeholder
|
|
49
|
+
self.max_selected_display = max_selected_display
|
|
50
|
+
self.show_search = show_search
|
|
51
|
+
|
|
52
|
+
# Validate icon_size
|
|
53
|
+
if icon_size not in ["small", "large"]:
|
|
54
|
+
self.icon_size = "small"
|
|
55
|
+
else:
|
|
56
|
+
self.icon_size = icon_size
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def choices(self) -> list:
|
|
60
|
+
"""Get multi-options choices with ui_options as primary source of truth.
|
|
61
|
+
|
|
62
|
+
CRITICAL: This property prioritizes ui_options["multi_options"]["choices"] over _choices
|
|
63
|
+
because ui_options gets properly serialized/deserialized while trait fields don't.
|
|
64
|
+
|
|
65
|
+
Read priority:
|
|
66
|
+
1. FIRST: ui_options["multi_options"]["choices"] (survives serialization cycles)
|
|
67
|
+
2. FALLBACK: _choices field (safety net for edge cases)
|
|
68
|
+
|
|
69
|
+
This prevents bugs where available choices could become stale after reload.
|
|
70
|
+
"""
|
|
71
|
+
# Check if we have a parent parameter with ui_options (normal case after trait attachment)
|
|
72
|
+
if self._parent and hasattr(self._parent, "ui_options"):
|
|
73
|
+
ui_options = getattr(self._parent, "ui_options", None)
|
|
74
|
+
if (
|
|
75
|
+
isinstance(ui_options, dict)
|
|
76
|
+
and "multi_options" in ui_options
|
|
77
|
+
and isinstance(ui_options["multi_options"], dict)
|
|
78
|
+
and "choices" in ui_options["multi_options"]
|
|
79
|
+
):
|
|
80
|
+
# Use live ui_options data (this survives serialization)
|
|
81
|
+
return ui_options["multi_options"]["choices"]
|
|
82
|
+
|
|
83
|
+
# Fallback to internal field (used during initialization or if ui_options missing)
|
|
84
|
+
return self._choices
|
|
85
|
+
|
|
86
|
+
@choices.setter
|
|
87
|
+
def choices(self, value: list) -> None:
|
|
88
|
+
"""Set multi-options choices with dual synchronization.
|
|
89
|
+
|
|
90
|
+
CRITICAL: This setter writes to BOTH locations to maintain consistency:
|
|
91
|
+
1. _choices field (for fallback and ui_options_for_trait())
|
|
92
|
+
2. ui_options["multi_options"]["choices"] (for serialization and runtime use)
|
|
93
|
+
|
|
94
|
+
This dual sync ensures:
|
|
95
|
+
- Immediate runtime consistency
|
|
96
|
+
- Proper serialization of choices data
|
|
97
|
+
- Fallback safety if either location fails
|
|
98
|
+
"""
|
|
99
|
+
# Always update internal field first (provides fallback safety)
|
|
100
|
+
self._choices = value
|
|
101
|
+
|
|
102
|
+
# Sync to ui_options if we have a parent parameter (normal case after trait attachment)
|
|
103
|
+
if self._parent and hasattr(self._parent, "ui_options"):
|
|
104
|
+
ui_options = getattr(self._parent, "ui_options", None)
|
|
105
|
+
if not isinstance(ui_options, dict):
|
|
106
|
+
# Initialize ui_options if it doesn't exist or isn't a dict
|
|
107
|
+
self._parent.ui_options = {} # type: ignore[attr-defined]
|
|
108
|
+
|
|
109
|
+
# Ensure multi_options exists and is a dict
|
|
110
|
+
if "multi_options" not in self._parent.ui_options or not isinstance( # type: ignore[attr-defined]
|
|
111
|
+
self._parent.ui_options["multi_options"], # type: ignore[attr-defined]
|
|
112
|
+
dict,
|
|
113
|
+
):
|
|
114
|
+
self._parent.ui_options["multi_options"] = {} # type: ignore[attr-defined]
|
|
115
|
+
|
|
116
|
+
# Write choices to ui_options (this gets serialized and survives reload)
|
|
117
|
+
self._parent.ui_options["multi_options"]["choices"] = value # type: ignore[attr-defined]
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def get_trait_keys(cls) -> list[str]:
|
|
121
|
+
return ["multi_options"]
|
|
122
|
+
|
|
123
|
+
def converters_for_trait(self) -> list[Callable]:
|
|
124
|
+
def converter(value: Any) -> Any:
|
|
125
|
+
# CRITICAL: This converter uses self.choices property (not _choices field)
|
|
126
|
+
# The property reads from ui_options first, ensuring we use post-deserialization
|
|
127
|
+
# choices data instead of stale trait field data.
|
|
128
|
+
|
|
129
|
+
# Handle case where value is not a list (convert single values to list)
|
|
130
|
+
if not isinstance(value, list):
|
|
131
|
+
if value is None:
|
|
132
|
+
return []
|
|
133
|
+
value = [value]
|
|
134
|
+
|
|
135
|
+
# Filter out invalid choices and return valid ones
|
|
136
|
+
valid_choices = [v for v in value if v in self.choices]
|
|
137
|
+
|
|
138
|
+
# If no valid choices, return empty list (allow empty selection)
|
|
139
|
+
return valid_choices
|
|
140
|
+
|
|
141
|
+
return [converter]
|
|
142
|
+
|
|
143
|
+
def validators_for_trait(self) -> list[Callable[[Parameter, Any], Any]]:
|
|
144
|
+
def validator(param: Parameter, value: Any) -> None: # noqa: ARG001
|
|
145
|
+
# CRITICAL: This validator uses self.choices property (not _choices field)
|
|
146
|
+
# Same reasoning as converter - use live ui_options data after deserialization
|
|
147
|
+
|
|
148
|
+
# Allow None or empty list as valid (no selection)
|
|
149
|
+
if value is None or value == []:
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# Ensure value is a list
|
|
153
|
+
if not isinstance(value, list):
|
|
154
|
+
msg = "MultiOptions value must be a list"
|
|
155
|
+
raise TypeError(msg)
|
|
156
|
+
|
|
157
|
+
# Check that all selected values are valid choices
|
|
158
|
+
invalid_choices = [v for v in value if v not in self.choices]
|
|
159
|
+
if invalid_choices:
|
|
160
|
+
msg = f"Invalid choices: {invalid_choices}"
|
|
161
|
+
raise ValueError(msg)
|
|
162
|
+
|
|
163
|
+
return [validator]
|
|
164
|
+
|
|
165
|
+
def ui_options_for_trait(self) -> dict:
|
|
166
|
+
"""Provide UI options for trait initialization.
|
|
167
|
+
|
|
168
|
+
IMPORTANT: This method uses _choices (not self.choices property) to avoid
|
|
169
|
+
circular dependency during Parameter.ui_options property construction:
|
|
170
|
+
|
|
171
|
+
Circular dependency would be:
|
|
172
|
+
1. Parameter.ui_options calls trait.ui_options_for_trait()
|
|
173
|
+
2. ui_options_for_trait() calls self.choices property
|
|
174
|
+
3. choices property tries to read parent.ui_options
|
|
175
|
+
4. This triggers Parameter.ui_options again → infinite recursion
|
|
176
|
+
|
|
177
|
+
Using _choices directly breaks this cycle while still providing the correct
|
|
178
|
+
initial choices for UI rendering. The property-based sync handles runtime updates.
|
|
179
|
+
"""
|
|
180
|
+
return {
|
|
181
|
+
"multi_options": {
|
|
182
|
+
"choices": self._choices,
|
|
183
|
+
"placeholder": self.placeholder,
|
|
184
|
+
"max_selected_display": self.max_selected_display,
|
|
185
|
+
"show_search": self.show_search,
|
|
186
|
+
"icon_size": self.icon_size,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from griptape_nodes.exe_types.core_types import Parameter, ParameterMode, Trait
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(eq=False)
|
|
9
|
+
class NumbersSelector(Trait):
|
|
10
|
+
defaults: dict[str, float] = field(kw_only=True)
|
|
11
|
+
step: float = 1.0
|
|
12
|
+
overall_min: float | None = None
|
|
13
|
+
overall_max: float | None = None
|
|
14
|
+
element_id: str = field(default_factory=lambda: "NumbersSelector")
|
|
15
|
+
|
|
16
|
+
_allowed_modes: set = field(default_factory=lambda: {ParameterMode.PROPERTY})
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
defaults: dict[str, float],
|
|
21
|
+
step: float = 1.0,
|
|
22
|
+
overall_min: float | None = None,
|
|
23
|
+
overall_max: float | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.defaults = defaults
|
|
27
|
+
self.step = step
|
|
28
|
+
self.overall_min = overall_min
|
|
29
|
+
self.overall_max = overall_max
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def get_trait_keys(cls) -> list[str]:
|
|
33
|
+
return ["numbers_selector"]
|
|
34
|
+
|
|
35
|
+
def ui_options_for_trait(self) -> dict:
|
|
36
|
+
return {
|
|
37
|
+
"numbers_selector": {
|
|
38
|
+
"step": self.step,
|
|
39
|
+
"overall_min": self.overall_min,
|
|
40
|
+
"overall_max": self.overall_max,
|
|
41
|
+
"defaults": self.defaults,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def display_options_for_trait(self) -> dict:
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
def converters_for_trait(self) -> list[Callable]:
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
def validators_for_trait(self) -> list[Callable[..., Any]]:
|
|
52
|
+
def validate(_param: Parameter, value: Any) -> None:
|
|
53
|
+
if value is None:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if not isinstance(value, dict):
|
|
57
|
+
msg = "NumbersSelector value must be a dictionary"
|
|
58
|
+
raise TypeError(msg)
|
|
59
|
+
|
|
60
|
+
for key, val in value.items():
|
|
61
|
+
if not isinstance(key, str):
|
|
62
|
+
msg = f"NumbersSelector keys must be strings, got {type(key)}"
|
|
63
|
+
raise TypeError(msg)
|
|
64
|
+
|
|
65
|
+
if not isinstance(val, (int, float)):
|
|
66
|
+
msg = f"NumbersSelector values must be numbers, got {type(val)} for key '{key}'"
|
|
67
|
+
raise TypeError(msg)
|
|
68
|
+
|
|
69
|
+
if self.overall_min is not None and val < self.overall_min:
|
|
70
|
+
msg = f"Value {val} for key '{key}' is below minimum {self.overall_min}"
|
|
71
|
+
raise ValueError(msg)
|
|
72
|
+
|
|
73
|
+
if self.overall_max is not None and val > self.overall_max:
|
|
74
|
+
msg = f"Value {val} for key '{key}' is above maximum {self.overall_max}"
|
|
75
|
+
raise ValueError(msg)
|
|
76
|
+
|
|
77
|
+
return [validate]
|
griptape_nodes/traits/options.py
CHANGED
|
@@ -7,15 +7,90 @@ from griptape_nodes.exe_types.core_types import Parameter, Trait
|
|
|
7
7
|
|
|
8
8
|
@dataclass(eq=False)
|
|
9
9
|
class Options(Trait):
|
|
10
|
-
|
|
10
|
+
# SERIALIZATION BUG FIX EXPLANATION:
|
|
11
|
+
#
|
|
12
|
+
# PROBLEM: Options trait had a serialization bug where dynamically populated dropdown
|
|
13
|
+
# lists would work correctly during runtime but revert to the first choice after
|
|
14
|
+
# save/reload cycles. This happened because:
|
|
15
|
+
# 1. trait.choices was the "source of truth" during runtime
|
|
16
|
+
# 2. ui_options["simple_dropdown"] was populated from trait.choices
|
|
17
|
+
# 3. Only ui_options gets serialized/deserialized (not trait fields)
|
|
18
|
+
# 4. After reload, trait.choices was stale but ui_options had correct data
|
|
19
|
+
# 5. Converters used stale trait.choices, causing values to revert to first choice
|
|
20
|
+
#
|
|
21
|
+
# SOLUTION: Make ui_options the primary source of truth, with _choices as fallback
|
|
22
|
+
# 1. choices property reads from ui_options["simple_dropdown"] when available
|
|
23
|
+
# 2. choices setter writes to BOTH _choices and ui_options (dual sync)
|
|
24
|
+
# 3. This ensures serialized ui_options data is used after deserialization
|
|
25
|
+
# 4. _choices provides safety fallback if ui_options is missing/corrupted
|
|
26
|
+
|
|
27
|
+
_choices: list = field(default_factory=lambda: ["choice 1", "choice 2", "choice 3"])
|
|
11
28
|
element_id: str = field(default_factory=lambda: "Options")
|
|
12
29
|
|
|
30
|
+
def __init__(self, *, choices: list | None = None) -> None:
|
|
31
|
+
super().__init__()
|
|
32
|
+
# Set choices through property to ensure dual sync from the start
|
|
33
|
+
if choices is not None:
|
|
34
|
+
self.choices = choices
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def choices(self) -> list:
|
|
38
|
+
"""Get dropdown choices with ui_options as primary source of truth.
|
|
39
|
+
|
|
40
|
+
CRITICAL: This property prioritizes ui_options["simple_dropdown"] over _choices
|
|
41
|
+
because ui_options gets properly serialized/deserialized while trait fields don't.
|
|
42
|
+
|
|
43
|
+
Read priority:
|
|
44
|
+
1. FIRST: ui_options["simple_dropdown"] (survives serialization cycles)
|
|
45
|
+
2. FALLBACK: _choices field (safety net for edge cases)
|
|
46
|
+
|
|
47
|
+
This fixes the bug where selected values reverted to first choice after reload.
|
|
48
|
+
"""
|
|
49
|
+
# Check if we have a parent parameter with ui_options (normal case after trait attachment)
|
|
50
|
+
if self._parent and hasattr(self._parent, "ui_options"):
|
|
51
|
+
ui_options = getattr(self._parent, "ui_options", None)
|
|
52
|
+
if isinstance(ui_options, dict) and "simple_dropdown" in ui_options:
|
|
53
|
+
# Use live ui_options data (this survives serialization)
|
|
54
|
+
return ui_options["simple_dropdown"]
|
|
55
|
+
|
|
56
|
+
# Fallback to internal field (used during initialization or if ui_options missing)
|
|
57
|
+
return self._choices
|
|
58
|
+
|
|
59
|
+
@choices.setter
|
|
60
|
+
def choices(self, value: list) -> None:
|
|
61
|
+
"""Set dropdown choices with dual synchronization.
|
|
62
|
+
|
|
63
|
+
CRITICAL: This setter writes to BOTH locations to maintain consistency:
|
|
64
|
+
1. _choices field (for fallback and ui_options_for_trait())
|
|
65
|
+
2. ui_options["simple_dropdown"] (for serialization and runtime use)
|
|
66
|
+
|
|
67
|
+
This dual sync ensures:
|
|
68
|
+
- Immediate runtime consistency
|
|
69
|
+
- Proper serialization of choices data
|
|
70
|
+
- Fallback safety if either location fails
|
|
71
|
+
"""
|
|
72
|
+
# Always update internal field first (provides fallback safety)
|
|
73
|
+
self._choices = value
|
|
74
|
+
|
|
75
|
+
# Sync to ui_options if we have a parent parameter (normal case after trait attachment)
|
|
76
|
+
if self._parent and hasattr(self._parent, "ui_options"):
|
|
77
|
+
ui_options = getattr(self._parent, "ui_options", None)
|
|
78
|
+
if not isinstance(ui_options, dict):
|
|
79
|
+
# Initialize ui_options if it doesn't exist or isn't a dict
|
|
80
|
+
self._parent.ui_options = {} # type: ignore[attr-defined]
|
|
81
|
+
# Write choices to ui_options (this gets serialized and survives reload)
|
|
82
|
+
self._parent.ui_options["simple_dropdown"] = value # type: ignore[attr-defined]
|
|
83
|
+
|
|
13
84
|
@classmethod
|
|
14
85
|
def get_trait_keys(cls) -> list[str]:
|
|
15
86
|
return ["options", "models"]
|
|
16
87
|
|
|
17
88
|
def converters_for_trait(self) -> list[Callable]:
|
|
18
89
|
def converter(value: Any) -> Any:
|
|
90
|
+
# CRITICAL: This converter uses self.choices property (not _choices field)
|
|
91
|
+
# The property reads from ui_options first, ensuring we use post-deserialization
|
|
92
|
+
# choices data instead of stale trait field data. This prevents the bug where
|
|
93
|
+
# selected values revert to first choice after save/reload.
|
|
19
94
|
if value not in self.choices:
|
|
20
95
|
return self.choices[0]
|
|
21
96
|
return value
|
|
@@ -24,6 +99,8 @@ class Options(Trait):
|
|
|
24
99
|
|
|
25
100
|
def validators_for_trait(self) -> list[Callable[[Parameter, Any], Any]]:
|
|
26
101
|
def validator(param: Parameter, value: Any) -> None: # noqa: ARG001
|
|
102
|
+
# CRITICAL: This validator uses self.choices property (not _choices field)
|
|
103
|
+
# Same reasoning as converter - use live ui_options data after deserialization
|
|
27
104
|
if value not in self.choices:
|
|
28
105
|
msg = "Choice not allowed"
|
|
29
106
|
raise ValueError(msg)
|
|
@@ -31,4 +108,18 @@ class Options(Trait):
|
|
|
31
108
|
return [validator]
|
|
32
109
|
|
|
33
110
|
def ui_options_for_trait(self) -> dict:
|
|
34
|
-
|
|
111
|
+
"""Provide UI options for trait initialization.
|
|
112
|
+
|
|
113
|
+
IMPORTANT: This method uses _choices (not self.choices property) to avoid
|
|
114
|
+
circular dependency during Parameter.ui_options property construction:
|
|
115
|
+
|
|
116
|
+
Circular dependency would be:
|
|
117
|
+
1. Parameter.ui_options calls trait.ui_options_for_trait()
|
|
118
|
+
2. ui_options_for_trait() calls self.choices property
|
|
119
|
+
3. choices property tries to read parent.ui_options
|
|
120
|
+
4. This triggers Parameter.ui_options again → infinite recursion
|
|
121
|
+
|
|
122
|
+
Using _choices directly breaks this cycle while still providing the correct
|
|
123
|
+
initial choices for UI rendering. The property-based sync handles runtime updates.
|
|
124
|
+
"""
|
|
125
|
+
return {"simple_dropdown": self._choices}
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import inspect
|
|
7
|
+
import logging
|
|
7
8
|
import subprocess
|
|
8
9
|
from typing import TYPE_CHECKING, Any
|
|
9
10
|
|
|
@@ -11,6 +12,9 @@ if TYPE_CHECKING:
|
|
|
11
12
|
from collections.abc import Callable, Sequence
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
14
18
|
async def call_function(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
|
|
15
19
|
"""Call a function, handling both sync and async cases.
|
|
16
20
|
|
|
@@ -87,3 +91,30 @@ async def subprocess_run(
|
|
|
87
91
|
)
|
|
88
92
|
|
|
89
93
|
return completed_process
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def cancel_subprocess(process: asyncio.subprocess.Process, name: str = "process") -> None:
|
|
97
|
+
"""Cancel a subprocess with graceful termination then force kill.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
process: The subprocess to cancel
|
|
101
|
+
name: Name/description for logging purposes
|
|
102
|
+
"""
|
|
103
|
+
if process.returncode is not None: # Process already terminated
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
process.terminate()
|
|
108
|
+
logger.info("Terminated %s", name)
|
|
109
|
+
|
|
110
|
+
# Give process a chance to terminate gracefully
|
|
111
|
+
try:
|
|
112
|
+
await asyncio.wait_for(process.wait(), timeout=5.0)
|
|
113
|
+
except TimeoutError:
|
|
114
|
+
# Force kill if it doesn't terminate
|
|
115
|
+
process.kill()
|
|
116
|
+
logger.info("Force killed %s", name)
|
|
117
|
+
await process.wait()
|
|
118
|
+
except ProcessLookupError:
|
|
119
|
+
# Process already terminated
|
|
120
|
+
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: griptape-nodes
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.54.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Dist: griptape>=1.8.2
|
|
6
6
|
Requires-Dist: pydantic>=2.10.6
|
|
@@ -19,6 +19,9 @@ Requires-Dist: mcp[ws]>=1.10.1
|
|
|
19
19
|
Requires-Dist: binaryornot>=0.4.4
|
|
20
20
|
Requires-Dist: pillow>=11.3.0
|
|
21
21
|
Requires-Dist: watchfiles>=1.1.0
|
|
22
|
+
Requires-Dist: typer>=0.15.0
|
|
23
|
+
Requires-Dist: huggingface-hub>=0.28.0
|
|
24
|
+
Requires-Dist: rich>=14.1.0
|
|
22
25
|
Requires-Dist: austin-dist>=3.7.0 ; extra == 'profiling'
|
|
23
26
|
Requires-Python: >=3.12.0, <3.13
|
|
24
27
|
Provides-Extra: profiling
|