griptape-nodes 0.60.4__py3-none-any.whl → 0.62.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 +19 -7
- 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 +87 -0
- griptape_nodes/common/project_templates/defaults/README.md +36 -0
- griptape_nodes/common/project_templates/directory.py +67 -0
- griptape_nodes/common/project_templates/loader.py +342 -0
- griptape_nodes/common/project_templates/project.py +252 -0
- griptape_nodes/common/project_templates/situation.py +143 -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 +528 -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 +399 -6
- griptape_nodes/retained_mode/managers/project_manager.py +1067 -0
- griptape_nodes/retained_mode/managers/settings.py +6 -0
- griptape_nodes/retained_mode/managers/sync_manager.py +4 -1
- griptape_nodes/retained_mode/managers/workflow_manager.py +8 -79
- griptape_nodes/traits/button.py +19 -0
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.62.0.dist-info}/METADATA +5 -3
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.62.0.dist-info}/RECORD +47 -32
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.62.0.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.62.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Directory definition for logical project directories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from griptape_nodes.common.project_templates.loader import YAMLLineInfo
|
|
11
|
+
from griptape_nodes.common.project_templates.validation import ProjectValidationInfo
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DirectoryDefinition(BaseModel):
|
|
15
|
+
"""Definition of a logical directory in the project."""
|
|
16
|
+
|
|
17
|
+
name: str = Field(description="Logical name (e.g., 'inputs', 'outputs')")
|
|
18
|
+
path_macro: str = Field(description="Path string (may contain macros/env vars)")
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def merge(
|
|
22
|
+
base: DirectoryDefinition,
|
|
23
|
+
overlay_data: dict[str, Any],
|
|
24
|
+
field_path: str,
|
|
25
|
+
validation_info: ProjectValidationInfo,
|
|
26
|
+
line_info: YAMLLineInfo,
|
|
27
|
+
) -> DirectoryDefinition:
|
|
28
|
+
"""Merge overlay fields onto base directory.
|
|
29
|
+
|
|
30
|
+
Field-level merge behavior:
|
|
31
|
+
- path_macro: Use overlay if present, else base
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
base: Complete base directory
|
|
35
|
+
overlay_data: Partial directory dict from overlay
|
|
36
|
+
field_path: Path for validation errors (e.g., "directories.inputs")
|
|
37
|
+
validation_info: Shared validation info
|
|
38
|
+
line_info: Line tracking from overlay
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
New merged DirectoryDefinition
|
|
42
|
+
"""
|
|
43
|
+
# Start with base fields
|
|
44
|
+
merged_data = {"name": base.name, "path_macro": base.path_macro}
|
|
45
|
+
|
|
46
|
+
# Apply overlay if present
|
|
47
|
+
if "path_macro" in overlay_data:
|
|
48
|
+
merged_data["path_macro"] = overlay_data["path_macro"]
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
return DirectoryDefinition.model_validate(merged_data)
|
|
52
|
+
except ValidationError as e:
|
|
53
|
+
# Convert Pydantic validation errors to our validation_info format
|
|
54
|
+
for error in e.errors():
|
|
55
|
+
error_field_path = ".".join(str(loc) for loc in error["loc"])
|
|
56
|
+
full_field_path = f"{field_path}.{error_field_path}"
|
|
57
|
+
message = error["msg"]
|
|
58
|
+
line_number = line_info.get_line(full_field_path)
|
|
59
|
+
|
|
60
|
+
validation_info.add_error(
|
|
61
|
+
field_path=full_field_path,
|
|
62
|
+
message=message,
|
|
63
|
+
line_number=line_number,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Return base on validation error (fault-tolerant)
|
|
67
|
+
return base
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""YAML loading with line number tracking for project templates.
|
|
2
|
+
|
|
3
|
+
Note: This module only handles YAML parsing. File I/O operations should be
|
|
4
|
+
handled by ProjectManager using ReadFileRequest/WriteFileRequest to ensure
|
|
5
|
+
proper long path handling on Windows and consistent error handling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
12
|
+
|
|
13
|
+
from pydantic import ValidationError
|
|
14
|
+
from ruamel.yaml import YAML
|
|
15
|
+
from ruamel.yaml.comments import CommentedMap, CommentedSeq
|
|
16
|
+
from ruamel.yaml.error import YAMLError
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from griptape_nodes.common.project_templates.project import ProjectTemplate
|
|
20
|
+
from griptape_nodes.common.project_templates.validation import ProjectValidationInfo
|
|
21
|
+
|
|
22
|
+
# Field name constants
|
|
23
|
+
FIELD_NAME = "name"
|
|
24
|
+
FIELD_SITUATIONS = "situations"
|
|
25
|
+
FIELD_DIRECTORIES = "directories"
|
|
26
|
+
FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION = "project_template_schema_version"
|
|
27
|
+
FIELD_ENVIRONMENT = "environment"
|
|
28
|
+
FIELD_DESCRIPTION = "description"
|
|
29
|
+
|
|
30
|
+
# Special constants
|
|
31
|
+
ROOT_FIELD_PATH = "<root>"
|
|
32
|
+
DEFAULT_SCHEMA_VERSION = "0.1.0"
|
|
33
|
+
DEFAULT_PROJECT_NAME = "Invalid Project"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class YAMLLineInfo:
|
|
38
|
+
"""Line number information for YAML fields."""
|
|
39
|
+
|
|
40
|
+
field_path_to_line: dict[str, int] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
def get_line(self, field_path: str) -> int | None:
|
|
43
|
+
"""Get line number for a field path, returns None if not tracked."""
|
|
44
|
+
return self.field_path_to_line.get(field_path)
|
|
45
|
+
|
|
46
|
+
def add_mapping(self, field_path: str, line_number: int) -> None:
|
|
47
|
+
"""Add a field path to line number mapping."""
|
|
48
|
+
self.field_path_to_line[field_path] = line_number
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class YAMLParseResult(NamedTuple):
|
|
52
|
+
"""Result of parsing YAML with line tracking.
|
|
53
|
+
|
|
54
|
+
Contains the parsed YAML data and line number tracking information.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
data: dict[str, Any]
|
|
58
|
+
line_info: YAMLLineInfo
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ProjectOverlayData(NamedTuple):
|
|
62
|
+
"""Partially validated project template data for merging.
|
|
63
|
+
|
|
64
|
+
Contains raw dicts for situations/directories with basic structural validation.
|
|
65
|
+
Line tracking preserved for error reporting during merge.
|
|
66
|
+
Used as overlay input to ProjectTemplate.merge().
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
project_template_schema_version: str
|
|
71
|
+
situations: dict[str, dict[str, Any]] # situation_name -> raw dict
|
|
72
|
+
directories: dict[str, dict[str, Any]] # directory_name -> raw dict
|
|
73
|
+
environment: dict[str, str]
|
|
74
|
+
description: str | None
|
|
75
|
+
line_info: YAMLLineInfo
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_yaml_with_line_tracking(yaml_text: str) -> YAMLParseResult:
|
|
79
|
+
"""Load YAML preserving line numbers and comments.
|
|
80
|
+
|
|
81
|
+
Uses ruamel.yaml to parse while tracking line numbers for each field.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
- Parsed YAML data as dict
|
|
85
|
+
- Line number tracking info
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
YAMLError: If YAML syntax is invalid (unclosed quotes, invalid structure, etc.)
|
|
89
|
+
"""
|
|
90
|
+
yaml = YAML()
|
|
91
|
+
yaml.preserve_quotes = True
|
|
92
|
+
yaml.default_flow_style = False
|
|
93
|
+
|
|
94
|
+
# Parse YAML with line tracking
|
|
95
|
+
# ruamel.yaml returns CommentedMap/CommentedSeq objects with line info
|
|
96
|
+
data = yaml.load(yaml_text)
|
|
97
|
+
|
|
98
|
+
if not isinstance(data, dict):
|
|
99
|
+
msg = f"Expected YAML root to be a mapping (dict), got {type(data).__name__}"
|
|
100
|
+
raise YAMLError(msg)
|
|
101
|
+
|
|
102
|
+
# Build line number mapping
|
|
103
|
+
line_info = YAMLLineInfo()
|
|
104
|
+
|
|
105
|
+
def track_lines(obj: CommentedMap | CommentedSeq, path: str) -> None:
|
|
106
|
+
"""Recursively track line numbers for YAML objects.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
obj: CommentedMap or CommentedSeq from ruamel.yaml (has lc attribute)
|
|
110
|
+
path: Current field path for tracking
|
|
111
|
+
|
|
112
|
+
Note: Only CommentedMap/CommentedSeq objects have line tracking.
|
|
113
|
+
Plain dicts/lists and scalars don't have lc attributes.
|
|
114
|
+
"""
|
|
115
|
+
# Track line number for this object
|
|
116
|
+
line = obj.lc.line + 1 # Convert to 1-indexed
|
|
117
|
+
line_info.add_mapping(path, line)
|
|
118
|
+
|
|
119
|
+
# Recurse into children
|
|
120
|
+
if isinstance(obj, CommentedMap):
|
|
121
|
+
for key, value in obj.items():
|
|
122
|
+
child_path = f"{path}.{key}" if path else key
|
|
123
|
+
if isinstance(value, CommentedMap | CommentedSeq):
|
|
124
|
+
track_lines(value, child_path)
|
|
125
|
+
elif isinstance(obj, CommentedSeq):
|
|
126
|
+
for i, item in enumerate(obj):
|
|
127
|
+
child_path = f"{path}[{i}]"
|
|
128
|
+
if isinstance(item, CommentedMap | CommentedSeq):
|
|
129
|
+
track_lines(item, child_path)
|
|
130
|
+
|
|
131
|
+
# At runtime, data is a CommentedMap from ruamel.yaml
|
|
132
|
+
if isinstance(data, CommentedMap):
|
|
133
|
+
track_lines(data, "")
|
|
134
|
+
|
|
135
|
+
return YAMLParseResult(data=data, line_info=line_info)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def load_project_template_from_yaml( # noqa: C901
|
|
139
|
+
yaml_text: str,
|
|
140
|
+
validation_info: ProjectValidationInfo,
|
|
141
|
+
) -> ProjectTemplate | None:
|
|
142
|
+
"""Parse project.yml text into ProjectTemplate.
|
|
143
|
+
|
|
144
|
+
Two-pass approach:
|
|
145
|
+
1. Load raw YAML with line tracking (may raise YAMLError)
|
|
146
|
+
2. Build ProjectTemplate via Pydantic validation, collecting validation problems
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
yaml_text: YAML text to parse
|
|
150
|
+
validation_info: Validation info to populate (caller-owned, will be mutated)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
ProjectTemplate on success, None if fatal errors prevent construction
|
|
154
|
+
"""
|
|
155
|
+
# Lazy import required: circular dependency between this module and project.py
|
|
156
|
+
# project.py imports ProjectOverlayData from this file, and we need ProjectTemplate from project.py
|
|
157
|
+
from griptape_nodes.common.project_templates.project import ProjectTemplate
|
|
158
|
+
|
|
159
|
+
# Pass 1: Load YAML with line tracking
|
|
160
|
+
try:
|
|
161
|
+
result = load_yaml_with_line_tracking(yaml_text)
|
|
162
|
+
except YAMLError as e:
|
|
163
|
+
# YAML syntax error - cannot proceed
|
|
164
|
+
validation_info.add_error(
|
|
165
|
+
field_path=ROOT_FIELD_PATH,
|
|
166
|
+
message=f"YAML syntax error: {e}",
|
|
167
|
+
line_number=None,
|
|
168
|
+
)
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
# Pass 2: Build ProjectTemplate with Pydantic validation
|
|
172
|
+
data = result.data
|
|
173
|
+
line_info = result.line_info
|
|
174
|
+
|
|
175
|
+
# Add names to situations and directories before validation
|
|
176
|
+
if FIELD_SITUATIONS in data and isinstance(data[FIELD_SITUATIONS], dict):
|
|
177
|
+
for sit_name, sit_data in data[FIELD_SITUATIONS].items():
|
|
178
|
+
if isinstance(sit_data, dict):
|
|
179
|
+
sit_data[FIELD_NAME] = sit_name
|
|
180
|
+
|
|
181
|
+
if FIELD_DIRECTORIES in data and isinstance(data[FIELD_DIRECTORIES], dict):
|
|
182
|
+
for dir_name, dir_data in data[FIELD_DIRECTORIES].items():
|
|
183
|
+
if isinstance(dir_data, dict):
|
|
184
|
+
dir_data[FIELD_NAME] = dir_name
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
template = ProjectTemplate.model_validate(data)
|
|
188
|
+
except ValidationError as e:
|
|
189
|
+
# Convert Pydantic validation errors to our validation_info format
|
|
190
|
+
for error in e.errors():
|
|
191
|
+
# Build field path from error location
|
|
192
|
+
field_path = ".".join(str(loc) for loc in error["loc"])
|
|
193
|
+
message = error["msg"]
|
|
194
|
+
|
|
195
|
+
# Try to get line number from our line tracking
|
|
196
|
+
line_number = line_info.get_line(field_path)
|
|
197
|
+
|
|
198
|
+
validation_info.add_error(
|
|
199
|
+
field_path=field_path,
|
|
200
|
+
message=message,
|
|
201
|
+
line_number=line_number,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return None
|
|
205
|
+
else:
|
|
206
|
+
# Add warnings for schema version mismatches
|
|
207
|
+
if template.project_template_schema_version != ProjectTemplate.LATEST_SCHEMA_VERSION:
|
|
208
|
+
message = (
|
|
209
|
+
f"Schema version '{template.project_template_schema_version}' "
|
|
210
|
+
f"differs from latest '{ProjectTemplate.LATEST_SCHEMA_VERSION}'"
|
|
211
|
+
)
|
|
212
|
+
validation_info.add_warning(
|
|
213
|
+
field_path=FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION,
|
|
214
|
+
message=message,
|
|
215
|
+
line_number=line_info.get_line(FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return template
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def load_partial_project_template(
|
|
222
|
+
yaml_text: str,
|
|
223
|
+
validation_info: ProjectValidationInfo,
|
|
224
|
+
) -> ProjectOverlayData | None:
|
|
225
|
+
"""Load project template overlay for merging without full construction.
|
|
226
|
+
|
|
227
|
+
Performs minimal structural validation:
|
|
228
|
+
- YAML syntax
|
|
229
|
+
- Required top-level fields (name, project_template_schema_version)
|
|
230
|
+
- Basic type checking (situations is dict, directories is dict, etc.)
|
|
231
|
+
|
|
232
|
+
Does NOT:
|
|
233
|
+
- Construct full SituationTemplate/DirectoryDefinition objects
|
|
234
|
+
- Validate situation macros or policy values
|
|
235
|
+
- Check directory references
|
|
236
|
+
|
|
237
|
+
Use this for loading user overlay templates before merge.
|
|
238
|
+
After merge, full validation happens during object construction.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
yaml_text: YAML text to parse
|
|
242
|
+
validation_info: Validation info to populate (caller-owned, will be mutated)
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
ProjectOverlayData on success, None if fatal errors prevent construction
|
|
246
|
+
"""
|
|
247
|
+
# Parse YAML with line tracking
|
|
248
|
+
try:
|
|
249
|
+
result = load_yaml_with_line_tracking(yaml_text)
|
|
250
|
+
data = result.data
|
|
251
|
+
line_info = result.line_info
|
|
252
|
+
except YAMLError as e:
|
|
253
|
+
validation_info.add_error(
|
|
254
|
+
field_path=ROOT_FIELD_PATH,
|
|
255
|
+
message=f"YAML syntax error: {e}",
|
|
256
|
+
line_number=None,
|
|
257
|
+
)
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
# Validate required field: name
|
|
261
|
+
name = data.get(FIELD_NAME)
|
|
262
|
+
if name is None:
|
|
263
|
+
validation_info.add_error(
|
|
264
|
+
field_path=FIELD_NAME,
|
|
265
|
+
message=f"Required field '{FIELD_NAME}' missing",
|
|
266
|
+
line_number=line_info.get_line(FIELD_NAME),
|
|
267
|
+
)
|
|
268
|
+
name = DEFAULT_PROJECT_NAME
|
|
269
|
+
elif not isinstance(name, str):
|
|
270
|
+
validation_info.add_error(
|
|
271
|
+
field_path=FIELD_NAME,
|
|
272
|
+
message=f"Field '{FIELD_NAME}' must be string, got {type(name).__name__}",
|
|
273
|
+
line_number=line_info.get_line(FIELD_NAME),
|
|
274
|
+
)
|
|
275
|
+
name = DEFAULT_PROJECT_NAME
|
|
276
|
+
|
|
277
|
+
# Validate required field: project_template_schema_version
|
|
278
|
+
schema_version = data.get(FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION)
|
|
279
|
+
if schema_version is None:
|
|
280
|
+
validation_info.add_error(
|
|
281
|
+
field_path=FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION,
|
|
282
|
+
message=f"Required field '{FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION}' missing",
|
|
283
|
+
line_number=line_info.get_line(FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION),
|
|
284
|
+
)
|
|
285
|
+
schema_version = DEFAULT_SCHEMA_VERSION
|
|
286
|
+
elif not isinstance(schema_version, str):
|
|
287
|
+
validation_info.add_error(
|
|
288
|
+
field_path=FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION,
|
|
289
|
+
message=f"Must be string, got {type(schema_version).__name__}",
|
|
290
|
+
line_number=line_info.get_line(FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION),
|
|
291
|
+
)
|
|
292
|
+
schema_version = DEFAULT_SCHEMA_VERSION
|
|
293
|
+
|
|
294
|
+
# Optional field: situations (default to empty dict)
|
|
295
|
+
situations = data.get(FIELD_SITUATIONS, {})
|
|
296
|
+
if not isinstance(situations, dict):
|
|
297
|
+
validation_info.add_error(
|
|
298
|
+
field_path=FIELD_SITUATIONS,
|
|
299
|
+
message=f"Must be dict, got {type(situations).__name__}",
|
|
300
|
+
line_number=line_info.get_line(FIELD_SITUATIONS),
|
|
301
|
+
)
|
|
302
|
+
situations = {}
|
|
303
|
+
|
|
304
|
+
# Optional field: directories (default to empty dict)
|
|
305
|
+
directories = data.get(FIELD_DIRECTORIES, {})
|
|
306
|
+
if not isinstance(directories, dict):
|
|
307
|
+
validation_info.add_error(
|
|
308
|
+
field_path=FIELD_DIRECTORIES,
|
|
309
|
+
message=f"Must be dict, got {type(directories).__name__}",
|
|
310
|
+
line_number=line_info.get_line(FIELD_DIRECTORIES),
|
|
311
|
+
)
|
|
312
|
+
directories = {}
|
|
313
|
+
|
|
314
|
+
# Optional field: environment (default to empty dict)
|
|
315
|
+
environment = data.get(FIELD_ENVIRONMENT, {})
|
|
316
|
+
if not isinstance(environment, dict):
|
|
317
|
+
validation_info.add_error(
|
|
318
|
+
field_path=FIELD_ENVIRONMENT,
|
|
319
|
+
message=f"Must be dict, got {type(environment).__name__}",
|
|
320
|
+
line_number=line_info.get_line(FIELD_ENVIRONMENT),
|
|
321
|
+
)
|
|
322
|
+
environment = {}
|
|
323
|
+
|
|
324
|
+
# Optional field: description
|
|
325
|
+
description = data.get(FIELD_DESCRIPTION)
|
|
326
|
+
if description is not None and not isinstance(description, str):
|
|
327
|
+
validation_info.add_error(
|
|
328
|
+
field_path=FIELD_DESCRIPTION,
|
|
329
|
+
message=f"Must be string, got {type(description).__name__}",
|
|
330
|
+
line_number=line_info.get_line(FIELD_DESCRIPTION),
|
|
331
|
+
)
|
|
332
|
+
description = None
|
|
333
|
+
|
|
334
|
+
return ProjectOverlayData(
|
|
335
|
+
name=name,
|
|
336
|
+
project_template_schema_version=schema_version,
|
|
337
|
+
situations=situations,
|
|
338
|
+
directories=directories,
|
|
339
|
+
environment=environment,
|
|
340
|
+
description=description,
|
|
341
|
+
line_info=line_info,
|
|
342
|
+
)
|
|
@@ -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
|
+
)
|