griptape-nodes 0.60.3__py3-none-any.whl → 0.61.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.
Files changed (47) hide show
  1. griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +0 -1
  2. griptape_nodes/common/macro_parser/__init__.py +16 -1
  3. griptape_nodes/common/macro_parser/core.py +15 -3
  4. griptape_nodes/common/macro_parser/exceptions.py +99 -0
  5. griptape_nodes/common/macro_parser/formats.py +13 -4
  6. griptape_nodes/common/macro_parser/matching.py +5 -2
  7. griptape_nodes/common/macro_parser/parsing.py +48 -8
  8. griptape_nodes/common/macro_parser/resolution.py +23 -5
  9. griptape_nodes/common/project_templates/__init__.py +49 -0
  10. griptape_nodes/common/project_templates/default_project_template.py +92 -0
  11. griptape_nodes/common/project_templates/defaults/README.md +36 -0
  12. griptape_nodes/common/project_templates/defaults/project_template.yml +89 -0
  13. griptape_nodes/common/project_templates/directory.py +67 -0
  14. griptape_nodes/common/project_templates/loader.py +341 -0
  15. griptape_nodes/common/project_templates/project.py +252 -0
  16. griptape_nodes/common/project_templates/situation.py +155 -0
  17. griptape_nodes/common/project_templates/validation.py +140 -0
  18. griptape_nodes/exe_types/core_types.py +36 -3
  19. griptape_nodes/exe_types/node_types.py +4 -2
  20. griptape_nodes/exe_types/param_components/progress_bar_component.py +57 -0
  21. griptape_nodes/exe_types/param_types/parameter_audio.py +243 -0
  22. griptape_nodes/exe_types/param_types/parameter_image.py +243 -0
  23. griptape_nodes/exe_types/param_types/parameter_three_d.py +215 -0
  24. griptape_nodes/exe_types/param_types/parameter_video.py +243 -0
  25. griptape_nodes/node_library/workflow_registry.py +1 -1
  26. griptape_nodes/retained_mode/events/execution_events.py +41 -0
  27. griptape_nodes/retained_mode/events/node_events.py +90 -1
  28. griptape_nodes/retained_mode/events/os_events.py +108 -0
  29. griptape_nodes/retained_mode/events/parameter_events.py +1 -1
  30. griptape_nodes/retained_mode/events/project_events.py +413 -0
  31. griptape_nodes/retained_mode/events/workflow_events.py +19 -1
  32. griptape_nodes/retained_mode/griptape_nodes.py +9 -1
  33. griptape_nodes/retained_mode/managers/agent_manager.py +18 -24
  34. griptape_nodes/retained_mode/managers/event_manager.py +6 -9
  35. griptape_nodes/retained_mode/managers/flow_manager.py +63 -0
  36. griptape_nodes/retained_mode/managers/library_manager.py +55 -42
  37. griptape_nodes/retained_mode/managers/mcp_manager.py +14 -6
  38. griptape_nodes/retained_mode/managers/node_manager.py +232 -0
  39. griptape_nodes/retained_mode/managers/os_manager.py +346 -1
  40. griptape_nodes/retained_mode/managers/project_manager.py +617 -0
  41. griptape_nodes/retained_mode/managers/settings.py +6 -0
  42. griptape_nodes/retained_mode/managers/workflow_manager.py +17 -71
  43. griptape_nodes/traits/button.py +18 -0
  44. {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/METADATA +5 -3
  45. {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/RECORD +47 -31
  46. {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/WHEEL +1 -1
  47. {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,252 @@
1
+ """Project template main class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ from typing import TYPE_CHECKING, ClassVar
7
+
8
+ from pydantic import BaseModel, Field, ValidationError
9
+ from ruamel.yaml import YAML
10
+
11
+ from griptape_nodes.common.project_templates.directory import DirectoryDefinition
12
+ from griptape_nodes.common.project_templates.situation import SituationTemplate
13
+ from griptape_nodes.common.project_templates.validation import (
14
+ ProjectOverrideAction,
15
+ ProjectOverrideCategory,
16
+ ProjectValidationInfo,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from griptape_nodes.common.project_templates.loader import ProjectOverlayData
21
+
22
+
23
+ class ProjectTemplate(BaseModel):
24
+ """Complete project template loaded from project.yml."""
25
+
26
+ LATEST_SCHEMA_VERSION: ClassVar[str] = "0.1.0"
27
+
28
+ project_template_schema_version: str = Field(description="Schema version for the project template")
29
+ name: str = Field(description="Name of the project")
30
+ situations: dict[str, SituationTemplate] = Field(description="Situation templates (situation_name -> template)")
31
+ directories: dict[str, DirectoryDefinition] = Field(
32
+ description="Directory definitions (logical_name -> definition)"
33
+ )
34
+ environment: dict[str, str] = Field(default_factory=dict, description="Custom environment variables")
35
+ description: str | None = Field(default=None, description="Description of the project")
36
+
37
+ def get_situation(self, situation_name: str) -> SituationTemplate | None:
38
+ """Get a situation by name, returns None if not found."""
39
+ return self.situations.get(situation_name)
40
+
41
+ def get_directory(self, directory_name: str) -> DirectoryDefinition | None:
42
+ """Get a directory definition by logical name."""
43
+ return self.directories.get(directory_name)
44
+
45
+ def to_yaml(self, *, include_comments: bool = True) -> str:
46
+ """Export project template to YAML string.
47
+
48
+ If include_comments=True, adds helpful comments explaining each section.
49
+ """
50
+ yaml = YAML()
51
+ yaml.preserve_quotes = True
52
+ yaml.default_flow_style = False
53
+
54
+ data = self.model_dump(mode="json", exclude_none=True)
55
+
56
+ # Convert to YAML string
57
+ stream = io.StringIO()
58
+ yaml.dump(data, stream)
59
+ yaml_text = stream.getvalue()
60
+
61
+ if include_comments:
62
+ # Add helpful header comment
63
+ header = (
64
+ "# Project Template\n"
65
+ f"# Version: {self.project_template_schema_version}\n"
66
+ "#\n"
67
+ "# This file defines how files are organized and saved in your project.\n"
68
+ "# See documentation for details on customizing situations and directories.\n\n"
69
+ )
70
+ yaml_text = header + yaml_text
71
+
72
+ return yaml_text
73
+
74
+ @staticmethod
75
+ def merge( # noqa: C901, PLR0912
76
+ base: ProjectTemplate,
77
+ overlay: ProjectOverlayData,
78
+ validation_info: ProjectValidationInfo,
79
+ ) -> ProjectTemplate:
80
+ """Merge overlay data on top of base template.
81
+
82
+ Merge behavior:
83
+ - name: From overlay (required)
84
+ - description: From overlay if present, else base
85
+ - project_template_schema_version: From overlay (required)
86
+ - situations: Dict merge with field-level merging for conflicts
87
+ - directories: Dict merge with field-level merging for conflicts
88
+ - environment: Dict merge (overlay values override base)
89
+
90
+ Override tracking (non-status-affecting):
91
+ - Metadata: name (always MODIFIED), description (if different)
92
+ - Situations: MODIFIED if exists in base, ADDED if new
93
+ - Directories: MODIFIED if exists in base, ADDED if new
94
+ - Environment: MODIFIED if exists in base, ADDED if new
95
+
96
+ Note: Schema version compatibility should be checked by caller (ProjectManager)
97
+ before calling merge. This method does not validate version compatibility.
98
+
99
+ Args:
100
+ base: Fully constructed base template (e.g., system defaults)
101
+ overlay: Partially validated overlay data with raw dicts
102
+ validation_info: Fresh ProjectValidationInfo for tracking overrides and errors
103
+
104
+ Returns:
105
+ New fully constructed merged ProjectTemplate with validation_info
106
+ """
107
+ # Track metadata overrides
108
+ validation_info.add_override(
109
+ category=ProjectOverrideCategory.METADATA,
110
+ name="name",
111
+ action=ProjectOverrideAction.MODIFIED,
112
+ )
113
+
114
+ if overlay.description is not None and overlay.description != base.description:
115
+ validation_info.add_override(
116
+ category=ProjectOverrideCategory.METADATA,
117
+ name="description",
118
+ action=ProjectOverrideAction.MODIFIED,
119
+ )
120
+
121
+ # Merge situations
122
+ merged_situations: dict[str, SituationTemplate] = {}
123
+
124
+ # Start with all base situations
125
+ for sit_name, base_sit in base.situations.items():
126
+ if sit_name in overlay.situations:
127
+ # Field-level merge
128
+ merged_sit = SituationTemplate.merge(
129
+ base=base_sit,
130
+ overlay_data=overlay.situations[sit_name],
131
+ field_path=f"situations.{sit_name}",
132
+ validation_info=validation_info,
133
+ line_info=overlay.line_info,
134
+ )
135
+ merged_situations[sit_name] = merged_sit
136
+
137
+ validation_info.add_override(
138
+ category=ProjectOverrideCategory.SITUATION,
139
+ name=sit_name,
140
+ action=ProjectOverrideAction.MODIFIED,
141
+ )
142
+ else:
143
+ # Inherit from base
144
+ merged_situations[sit_name] = base_sit
145
+
146
+ # Add new situations from overlay
147
+ for sit_name, sit_data in overlay.situations.items():
148
+ if sit_name not in base.situations:
149
+ # New situation - construct from scratch
150
+ # Add name to dict for model_validate
151
+ sit_data_with_name = {"name": sit_name, **sit_data}
152
+
153
+ try:
154
+ new_sit = SituationTemplate.model_validate(sit_data_with_name)
155
+ merged_situations[sit_name] = new_sit
156
+
157
+ validation_info.add_override(
158
+ category=ProjectOverrideCategory.SITUATION,
159
+ name=sit_name,
160
+ action=ProjectOverrideAction.ADDED,
161
+ )
162
+ except ValidationError as e:
163
+ # Convert Pydantic validation errors
164
+ for error in e.errors():
165
+ error_field_path = ".".join(str(loc) for loc in error["loc"])
166
+ full_field_path = f"situations.{sit_name}.{error_field_path}"
167
+ message = error["msg"]
168
+ line_number = overlay.line_info.get_line(full_field_path)
169
+
170
+ validation_info.add_error(
171
+ field_path=full_field_path,
172
+ message=message,
173
+ line_number=line_number,
174
+ )
175
+
176
+ # Merge directories
177
+ merged_directories: dict[str, DirectoryDefinition] = {}
178
+
179
+ for dir_name, base_dir in base.directories.items():
180
+ if dir_name in overlay.directories:
181
+ # Field-level merge
182
+ merged_dir = DirectoryDefinition.merge(
183
+ base=base_dir,
184
+ overlay_data=overlay.directories[dir_name],
185
+ field_path=f"directories.{dir_name}",
186
+ validation_info=validation_info,
187
+ line_info=overlay.line_info,
188
+ )
189
+ merged_directories[dir_name] = merged_dir
190
+
191
+ validation_info.add_override(
192
+ category=ProjectOverrideCategory.DIRECTORY,
193
+ name=dir_name,
194
+ action=ProjectOverrideAction.MODIFIED,
195
+ )
196
+ else:
197
+ # Inherit from base
198
+ merged_directories[dir_name] = base_dir
199
+
200
+ # Add new directories from overlay
201
+ for dir_name, dir_data in overlay.directories.items():
202
+ if dir_name not in base.directories:
203
+ # New directory - construct from scratch
204
+ # Add name to dict for model_validate
205
+ dir_data_with_name = {"name": dir_name, **dir_data}
206
+
207
+ try:
208
+ new_dir = DirectoryDefinition.model_validate(dir_data_with_name)
209
+ merged_directories[dir_name] = new_dir
210
+
211
+ validation_info.add_override(
212
+ category=ProjectOverrideCategory.DIRECTORY,
213
+ name=dir_name,
214
+ action=ProjectOverrideAction.ADDED,
215
+ )
216
+ except ValidationError as e:
217
+ # Convert Pydantic validation errors
218
+ for error in e.errors():
219
+ error_field_path = ".".join(str(loc) for loc in error["loc"])
220
+ full_field_path = f"directories.{dir_name}.{error_field_path}"
221
+ message = error["msg"]
222
+ line_number = overlay.line_info.get_line(full_field_path)
223
+
224
+ validation_info.add_error(
225
+ field_path=full_field_path,
226
+ message=message,
227
+ line_number=line_number,
228
+ )
229
+
230
+ # Merge environment
231
+ merged_environment = {**base.environment}
232
+ for key, value in overlay.environment.items():
233
+ action = ProjectOverrideAction.MODIFIED if key in base.environment else ProjectOverrideAction.ADDED
234
+ merged_environment[key] = value
235
+
236
+ validation_info.add_override(
237
+ category=ProjectOverrideCategory.ENVIRONMENT,
238
+ name=key,
239
+ action=action,
240
+ )
241
+
242
+ # Use overlay metadata, fall back to base for description
243
+ merged_description = overlay.description if overlay.description is not None else base.description
244
+
245
+ return ProjectTemplate(
246
+ project_template_schema_version=overlay.project_template_schema_version,
247
+ name=overlay.name,
248
+ situations=merged_situations,
249
+ directories=merged_directories,
250
+ environment=merged_environment,
251
+ description=merged_description,
252
+ )
@@ -0,0 +1,155 @@
1
+ """Situation template definitions for file path scenarios."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import StrEnum
6
+ from typing import TYPE_CHECKING, Any, ClassVar
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
9
+
10
+ from griptape_nodes.common.macro_parser import MacroSyntaxError, ParsedMacro
11
+
12
+ if TYPE_CHECKING:
13
+ from griptape_nodes.common.project_templates.loader import YAMLLineInfo
14
+ from griptape_nodes.common.project_templates.validation import ProjectValidationInfo
15
+
16
+
17
+ class SituationFilePolicy(StrEnum):
18
+ """Policy for handling file collisions in situations.
19
+
20
+ Maps to ExistingFilePolicy for file operations, except PROMPT which
21
+ triggers user interaction before determining final policy.
22
+ """
23
+
24
+ CREATE_NEW = "create_new" # Increment {_index} in schema
25
+ OVERWRITE = "overwrite" # Maps to ExistingFilePolicy.OVERWRITE
26
+ FAIL = "fail" # Maps to ExistingFilePolicy.FAIL
27
+ PROMPT = "prompt" # Special UI handling
28
+
29
+
30
+ class SituationPolicy(BaseModel):
31
+ """Policy for file operations in a situation."""
32
+
33
+ on_collision: SituationFilePolicy = Field(description="Policy for handling file collisions")
34
+ create_dirs: bool = Field(description="Whether to create directories automatically")
35
+
36
+
37
+ class SituationTemplate(BaseModel):
38
+ """Template defining how files are saved in a specific situation."""
39
+
40
+ LATEST_SCHEMA_VERSION: ClassVar[str] = "0.1.0"
41
+
42
+ model_config = ConfigDict(populate_by_name=True)
43
+
44
+ name: str = Field(description="Name of the situation")
45
+ situation_template_schema_version: str = Field(description="Schema version for this situation template")
46
+ macro: str = Field(
47
+ description="Macro template for file path", serialization_alias="schema", validation_alias="schema"
48
+ )
49
+ policy: SituationPolicy = Field(description="Policy for file operations")
50
+ fallback: str | None = Field(default=None, description="Name of fallback situation")
51
+ description: str | None = Field(default=None, description="Description of the situation")
52
+
53
+ @field_validator("macro")
54
+ @classmethod
55
+ def validate_macro_syntax(cls, v: str) -> str:
56
+ """Validate macro syntax using macro parser."""
57
+ try:
58
+ ParsedMacro(v)
59
+ except MacroSyntaxError as e:
60
+ msg = f"Invalid macro syntax: {e}"
61
+ raise ValueError(msg) from e
62
+ return v
63
+
64
+ @property
65
+ def schema(self) -> str:
66
+ """Alias for macro to maintain YAML compatibility (schema field in YAML)."""
67
+ return self.macro
68
+
69
+ @staticmethod
70
+ def merge(
71
+ base: SituationTemplate,
72
+ overlay_data: dict[str, Any],
73
+ field_path: str,
74
+ validation_info: ProjectValidationInfo,
75
+ line_info: YAMLLineInfo,
76
+ ) -> SituationTemplate:
77
+ """Merge overlay fields onto base situation.
78
+
79
+ Field-level merge behavior:
80
+ - schema: Use overlay if present, else base
81
+ - description: Use overlay if present, else base
82
+ - fallback: Use overlay if present, else base
83
+ - policy: Use overlay if present (must be complete), else base
84
+ - situation_template_schema_version: Use overlay if present, else base
85
+
86
+ Policy validation:
87
+ - If policy provided in overlay, must contain both on_collision and create_dirs
88
+ - Adds error to validation_info if incomplete
89
+
90
+ Args:
91
+ base: Complete base situation to start from
92
+ overlay_data: Partial situation dict from overlay YAML
93
+ field_path: Path for validation errors (e.g., "situations.save_file")
94
+ validation_info: Shared validation info
95
+ line_info: Line tracking from overlay YAML
96
+
97
+ Returns:
98
+ New merged SituationTemplate
99
+ """
100
+ # Start with base fields as dict
101
+ merged_data = base.model_dump()
102
+
103
+ # Apply overlay fields if present
104
+ if "schema" in overlay_data:
105
+ merged_data["schema"] = overlay_data["schema"]
106
+
107
+ if "description" in overlay_data:
108
+ merged_data["description"] = overlay_data["description"]
109
+
110
+ if "fallback" in overlay_data:
111
+ merged_data["fallback"] = overlay_data["fallback"]
112
+
113
+ if "situation_template_schema_version" in overlay_data:
114
+ merged_data["situation_template_schema_version"] = overlay_data["situation_template_schema_version"]
115
+
116
+ # Policy must be complete if provided
117
+ if "policy" in overlay_data:
118
+ policy = overlay_data["policy"]
119
+ if not isinstance(policy, dict):
120
+ validation_info.add_error(
121
+ field_path=f"{field_path}.policy",
122
+ message="Policy must be a dict",
123
+ line_number=line_info.get_line(f"{field_path}.policy"),
124
+ )
125
+ elif "on_collision" not in policy or "create_dirs" not in policy:
126
+ validation_info.add_error(
127
+ field_path=f"{field_path}.policy",
128
+ message="Policy must include both on_collision and create_dirs",
129
+ line_number=line_info.get_line(f"{field_path}.policy"),
130
+ )
131
+ else:
132
+ merged_data["policy"] = policy
133
+
134
+ # Build merged situation using model_validate
135
+ # Note: name field is not in overlay_data, use base.name
136
+ merged_data_with_name = {"name": base.name, **merged_data}
137
+
138
+ try:
139
+ return SituationTemplate.model_validate(merged_data_with_name)
140
+ except ValidationError as e:
141
+ # Convert Pydantic validation errors to our validation_info format
142
+ for error in e.errors():
143
+ error_field_path = ".".join(str(loc) for loc in error["loc"])
144
+ full_field_path = f"{field_path}.{error_field_path}"
145
+ message = error["msg"]
146
+ line_number = line_info.get_line(full_field_path)
147
+
148
+ validation_info.add_error(
149
+ field_path=full_field_path,
150
+ message=message,
151
+ line_number=line_number,
152
+ )
153
+
154
+ # Return base on validation error (fault-tolerant)
155
+ return base
@@ -0,0 +1,140 @@
1
+ """Validation infrastructure for project templates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import StrEnum
7
+
8
+
9
+ class ProjectValidationStatus(StrEnum):
10
+ """Status of project template validation.
11
+
12
+ Mirrors WorkflowStatus pattern from workflow_manager.py.
13
+ """
14
+
15
+ GOOD = "GOOD" # No errors detected
16
+ FLAWED = "FLAWED" # Some errors, but recoverable
17
+ UNUSABLE = "UNUSABLE" # Errors make template unusable
18
+ MISSING = "MISSING" # File not found
19
+
20
+
21
+ class ProjectValidationProblemSeverity(StrEnum):
22
+ """Severity level of a validation problem."""
23
+
24
+ ERROR = "error"
25
+ WARNING = "warning"
26
+
27
+
28
+ class ProjectOverrideCategory(StrEnum):
29
+ """Category of template override during merge."""
30
+
31
+ SITUATION = "situation"
32
+ DIRECTORY = "directory"
33
+ ENVIRONMENT = "environment"
34
+ METADATA = "metadata"
35
+
36
+
37
+ class ProjectOverrideAction(StrEnum):
38
+ """Action taken during template merge."""
39
+
40
+ MODIFIED = "modified" # Existed in base, changed in overlay
41
+ ADDED = "added" # New in overlay, not in base
42
+
43
+
44
+ @dataclass
45
+ class ProjectValidationProblem:
46
+ """Single validation problem with context."""
47
+
48
+ line_number: int | None
49
+ field_path: str # e.g., "situations.copy_external_file.schema"
50
+ message: str
51
+ severity: ProjectValidationProblemSeverity
52
+
53
+
54
+ @dataclass
55
+ class ProjectOverride:
56
+ """Record of a template override during merge.
57
+
58
+ Tracks what was customized in the overlay without affecting validation status.
59
+ """
60
+
61
+ category: ProjectOverrideCategory
62
+ name: str
63
+ action: ProjectOverrideAction
64
+
65
+
66
+ @dataclass
67
+ class ProjectValidationInfo:
68
+ """Validation result for a project template.
69
+
70
+ Shared across construction chain - problems are accumulated as
71
+ the template is built from YAML.
72
+
73
+ Overrides track what was customized during merge without affecting status.
74
+ """
75
+
76
+ status: ProjectValidationStatus
77
+ problems: list[ProjectValidationProblem] = field(default_factory=list)
78
+ overrides: list[ProjectOverride] = field(default_factory=list)
79
+
80
+ def add_error(self, field_path: str, message: str, line_number: int | None = None) -> None:
81
+ """Add an error to the problems list.
82
+
83
+ Automatically downgrades status to UNUSABLE unless already MISSING.
84
+ Early returns if status is MISSING.
85
+ """
86
+ if self.status == ProjectValidationStatus.MISSING:
87
+ return
88
+
89
+ self.problems.append(
90
+ ProjectValidationProblem(
91
+ line_number=line_number,
92
+ field_path=field_path,
93
+ message=message,
94
+ severity=ProjectValidationProblemSeverity.ERROR,
95
+ )
96
+ )
97
+ self.status = ProjectValidationStatus.UNUSABLE
98
+
99
+ def add_warning(self, field_path: str, message: str, line_number: int | None = None) -> None:
100
+ """Add a warning to the problems list.
101
+
102
+ Automatically downgrades status from GOOD to FLAWED if current status is GOOD.
103
+ Does not change status if already FLAWED or UNUSABLE.
104
+ Early returns if status is MISSING.
105
+ """
106
+ if self.status == ProjectValidationStatus.MISSING:
107
+ return
108
+
109
+ self.problems.append(
110
+ ProjectValidationProblem(
111
+ line_number=line_number,
112
+ field_path=field_path,
113
+ message=message,
114
+ severity=ProjectValidationProblemSeverity.WARNING,
115
+ )
116
+ )
117
+
118
+ if self.status == ProjectValidationStatus.GOOD:
119
+ self.status = ProjectValidationStatus.FLAWED
120
+
121
+ def add_override(
122
+ self,
123
+ category: ProjectOverrideCategory,
124
+ name: str,
125
+ action: ProjectOverrideAction,
126
+ ) -> None:
127
+ """Record an override without affecting validation status.
128
+
129
+ Used during template merge to track what was customized in the overlay.
130
+
131
+ Args:
132
+ category: Type of override (situation, directory, environment, metadata)
133
+ name: Name of the item that was overridden
134
+ action: Whether it was modified (existed in base) or added (new in overlay)
135
+ """
136
+ self.overrides.append(ProjectOverride(category=category, name=name, action=action))
137
+
138
+ def is_usable(self) -> bool:
139
+ """Check if template can be used (GOOD or FLAWED status)."""
140
+ return self.status in (ProjectValidationStatus.GOOD, ProjectValidationStatus.FLAWED)
@@ -787,7 +787,7 @@ class DeprecationMessage(ParameterMessage):
787
787
  kwargs.setdefault("traits", {})
788
788
  kwargs["traits"][Button(label=button_text, icon="plus", variant="secondary", on_click=migrate_function)] = None
789
789
 
790
- super().__init__(value=value, **kwargs)
790
+ super().__init__(value=value, button_text=button_text, **kwargs)
791
791
 
792
792
  def to_dict(self) -> dict:
793
793
  """Override to_dict to use element_type instead of class name.
@@ -804,9 +804,18 @@ class DeprecationMessage(ParameterMessage):
804
804
  class ParameterGroup(BaseNodeElement, UIOptionsMixin):
805
805
  """UI element for a group of parameters."""
806
806
 
807
- def __init__(self, name: str, ui_options: dict | None = None, **kwargs):
807
+ def __init__(self, name: str, ui_options: dict | None = None, *, collapsed: bool = False, **kwargs):
808
808
  super().__init__(name=name, **kwargs)
809
- self._ui_options = ui_options or {}
809
+ if ui_options is None:
810
+ ui_options = {}
811
+ else:
812
+ ui_options = ui_options.copy()
813
+
814
+ # Add collapsed to ui_options if it's True
815
+ if collapsed:
816
+ ui_options["collapsed"] = collapsed
817
+
818
+ self._ui_options = ui_options
810
819
 
811
820
  @property
812
821
  def ui_options(self) -> dict:
@@ -817,6 +826,30 @@ class ParameterGroup(BaseNodeElement, UIOptionsMixin):
817
826
  def ui_options(self, value: dict) -> None:
818
827
  self._ui_options = value
819
828
 
829
+ @property
830
+ def collapsed(self) -> bool:
831
+ """Get whether the parameter group is collapsed.
832
+
833
+ Returns:
834
+ True if the group is collapsed, False otherwise
835
+ """
836
+ return self._ui_options.get("collapsed", False)
837
+
838
+ @collapsed.setter
839
+ @BaseNodeElement.emits_update_on_write
840
+ def collapsed(self, value: bool) -> None:
841
+ """Set whether the parameter group is collapsed.
842
+
843
+ Args:
844
+ value: Whether to collapse the group
845
+ """
846
+ if value:
847
+ self.update_ui_options_key("collapsed", value)
848
+ else:
849
+ ui_options = self._ui_options.copy()
850
+ ui_options.pop("collapsed", None)
851
+ self._ui_options = ui_options
852
+
820
853
  def to_dict(self) -> dict[str, Any]:
821
854
  """Returns a nested dictionary representation of this node and its children.
822
855
 
@@ -793,13 +793,15 @@ class BaseNode(ABC):
793
793
 
794
794
  def get_parameter_value(self, param_name: str) -> Any:
795
795
  param = self.get_parameter_by_name(param_name)
796
- if param and isinstance(param, ParameterContainer):
796
+ if param is None:
797
+ return None
798
+ if isinstance(param, ParameterContainer):
797
799
  value = handle_container_parameter(self, param)
798
800
  if value is not None:
799
801
  return value
800
802
  if param_name in self.parameter_values:
801
803
  return self.parameter_values[param_name]
802
- return param.default_value if param else None
804
+ return param.default_value
803
805
 
804
806
  def get_parameter_list_value(self, param: str) -> list:
805
807
  """Flattens the given param from self.params into a single list.
@@ -0,0 +1,57 @@
1
+ import logging
2
+
3
+ from griptape_nodes.exe_types.core_types import Parameter, ParameterMode
4
+ from griptape_nodes.exe_types.node_types import BaseNode
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class ProgressBarComponent:
10
+ def __init__(self, node: BaseNode):
11
+ self._node = node
12
+ self._total_steps = 0
13
+ self._current_step = 0
14
+
15
+ def add_property_parameters(self) -> None:
16
+ self._node.add_parameter(
17
+ Parameter(
18
+ name="progress",
19
+ output_type="float",
20
+ allowed_modes={ParameterMode.PROPERTY},
21
+ tooltip="Progress bar showing completion (0.0 to 1.0)",
22
+ ui_options={"progress_bar": True},
23
+ settable=False,
24
+ )
25
+ )
26
+
27
+ def initialize(self, total_steps: int) -> None:
28
+ """Initialize the progress bar with a total number of steps."""
29
+ self._total_steps = total_steps
30
+ self._current_step = 0
31
+ self._update_progress()
32
+
33
+ def increment(self, steps: int = 1) -> None:
34
+ """Increment the progress by the specified number of steps."""
35
+ self._current_step += steps
36
+ if self._current_step > self._total_steps:
37
+ logger.warning(
38
+ "Current step %i exceeds total steps %i. Progress will not exceed 100%.",
39
+ self._current_step,
40
+ self._total_steps,
41
+ )
42
+ self._update_progress()
43
+
44
+ def reset(self) -> None:
45
+ """Reset the progress bar to 0."""
46
+ self._current_step = 0
47
+ self._total_steps = 0
48
+ self._update_progress()
49
+
50
+ def _update_progress(self) -> None:
51
+ """Update the progress parameter based on current step and total steps."""
52
+ if self._total_steps <= 0:
53
+ progress_value = 0.0
54
+ else:
55
+ progress_value = min(1.0, self._current_step / self._total_steps)
56
+
57
+ self._node.publish_update_to_parameter("progress", progress_value)