griptape-nodes 0.60.4__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.
- griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +0 -1
- griptape_nodes/common/macro_parser/__init__.py +16 -1
- griptape_nodes/common/macro_parser/core.py +15 -3
- griptape_nodes/common/macro_parser/exceptions.py +99 -0
- griptape_nodes/common/macro_parser/formats.py +13 -4
- griptape_nodes/common/macro_parser/matching.py +5 -2
- griptape_nodes/common/macro_parser/parsing.py +48 -8
- griptape_nodes/common/macro_parser/resolution.py +23 -5
- griptape_nodes/common/project_templates/__init__.py +49 -0
- griptape_nodes/common/project_templates/default_project_template.py +92 -0
- griptape_nodes/common/project_templates/defaults/README.md +36 -0
- griptape_nodes/common/project_templates/defaults/project_template.yml +89 -0
- griptape_nodes/common/project_templates/directory.py +67 -0
- griptape_nodes/common/project_templates/loader.py +341 -0
- griptape_nodes/common/project_templates/project.py +252 -0
- griptape_nodes/common/project_templates/situation.py +155 -0
- griptape_nodes/common/project_templates/validation.py +140 -0
- griptape_nodes/exe_types/core_types.py +36 -3
- griptape_nodes/exe_types/node_types.py +4 -2
- griptape_nodes/exe_types/param_components/progress_bar_component.py +57 -0
- griptape_nodes/exe_types/param_types/parameter_audio.py +243 -0
- griptape_nodes/exe_types/param_types/parameter_image.py +243 -0
- griptape_nodes/exe_types/param_types/parameter_three_d.py +215 -0
- griptape_nodes/exe_types/param_types/parameter_video.py +243 -0
- griptape_nodes/node_library/workflow_registry.py +1 -1
- griptape_nodes/retained_mode/events/execution_events.py +41 -0
- griptape_nodes/retained_mode/events/node_events.py +90 -1
- griptape_nodes/retained_mode/events/os_events.py +108 -0
- griptape_nodes/retained_mode/events/parameter_events.py +1 -1
- griptape_nodes/retained_mode/events/project_events.py +413 -0
- griptape_nodes/retained_mode/events/workflow_events.py +19 -1
- griptape_nodes/retained_mode/griptape_nodes.py +9 -1
- griptape_nodes/retained_mode/managers/agent_manager.py +18 -24
- griptape_nodes/retained_mode/managers/event_manager.py +6 -9
- griptape_nodes/retained_mode/managers/flow_manager.py +63 -0
- griptape_nodes/retained_mode/managers/library_manager.py +55 -42
- griptape_nodes/retained_mode/managers/mcp_manager.py +14 -6
- griptape_nodes/retained_mode/managers/node_manager.py +232 -0
- griptape_nodes/retained_mode/managers/os_manager.py +345 -0
- griptape_nodes/retained_mode/managers/project_manager.py +617 -0
- griptape_nodes/retained_mode/managers/settings.py +6 -0
- griptape_nodes/retained_mode/managers/workflow_manager.py +6 -69
- griptape_nodes/traits/button.py +18 -0
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/METADATA +5 -3
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/RECORD +47 -31
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.60.4.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
|
-
|
|
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
|
|
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
|
|
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)
|