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,89 @@
1
+ # System Default Project Template
2
+ # Version: 0.1.0
3
+ #
4
+ # This is the default configuration used when no project.yml exists.
5
+ # You can copy this file to your project root and customize it.
6
+
7
+ project_template_schema_version: "0.1.0"
8
+ name: "Default Project"
9
+ description: "System default configuration"
10
+
11
+ # Directory Definitions
12
+ #
13
+ # Define logical directories used in situation schemas.
14
+ # Paths can be:
15
+ # - Relative: "inputs", "outputs/renders"
16
+ # - Absolute: "/tmp/cache", "C:/Projects/assets"
17
+ # - With macros: Path schemas will be resolved by ProjectManager
18
+ directories:
19
+ inputs:
20
+ path_schema: "inputs"
21
+ outputs:
22
+ path_schema: "outputs"
23
+ temp:
24
+ path_schema: "temp"
25
+ previews:
26
+ path_schema: "previews"
27
+
28
+ # Environment Variables (Optional)
29
+ #
30
+ # Define project-specific variables or document which system env vars are used.
31
+ # System automatically detects all env vars - this section is for documentation
32
+ # and project-specific overrides.
33
+ environment: {}
34
+
35
+ # Situation Configurations
36
+ #
37
+ # Each situation defines how files are saved in a specific scenario.
38
+ # The schema uses macro syntax: {variable_name} or {variable_name?} for optional
39
+ situations:
40
+
41
+ # Base situation - others fall back to this
42
+ save_file:
43
+ situation_template_schema_version: "0.1.0"
44
+ description: "Generic file save operation"
45
+ schema: "{file_name_base}{_index:03?}.{file_extension}"
46
+ policy:
47
+ on_collision: "create_new" # Increment {_index}: file_001.jpg, file_002.jpg
48
+ create_dirs: true
49
+ fallback: null
50
+
51
+ # User copies external file to project
52
+ copy_external_file:
53
+ situation_template_schema_version: "0.1.0"
54
+ description: "User copies external file to project"
55
+ schema: "{inputs}/{node_name?:_}{parameter_name?:_}{file_name_base}{_index:03?}.{file_extension}"
56
+ policy:
57
+ on_collision: "create_new"
58
+ create_dirs: true
59
+ fallback: "save_file"
60
+
61
+ # Download file from URL
62
+ download_url:
63
+ situation_template_schema_version: "0.1.0"
64
+ description: "Download file from URL"
65
+ schema: "{inputs}/{sanitized_url}"
66
+ policy:
67
+ on_collision: "overwrite" # Same URL = same content, safe to replace
68
+ create_dirs: true
69
+ fallback: "save_file"
70
+
71
+ # Node generates and saves output
72
+ save_node_output:
73
+ situation_template_schema_version: "0.1.0"
74
+ description: "Node generates and saves output"
75
+ schema: "{outputs}/{node_name?:_}{file_name_base}{_index:03?}.{file_extension}"
76
+ policy:
77
+ on_collision: "create_new"
78
+ create_dirs: true
79
+ fallback: "save_file"
80
+
81
+ # Generate preview/thumbnail
82
+ save_preview:
83
+ situation_template_schema_version: "0.1.0"
84
+ description: "Generate preview/thumbnail"
85
+ schema: "{previews}/{original_file_path}"
86
+ policy:
87
+ on_collision: "overwrite" # Preview should match source, safe to replace
88
+ create_dirs: true
89
+ fallback: "save_file"
@@ -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_schema: 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_schema: 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_schema": base.path_schema}
45
+
46
+ # Apply overlay if present
47
+ if "path_schema" in overlay_data:
48
+ merged_data["path_schema"] = overlay_data["path_schema"]
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,341 @@
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
+ # Import here to avoid circular dependency:
156
+ # - loader.py defines YAMLLineInfo
157
+ # - project.py imports YAMLLineInfo for type hints
158
+ # - loader.py needs ProjectTemplate at runtime
159
+ # TYPE_CHECKING import at top is for type hints only, this is for runtime
160
+ from griptape_nodes.common.project_templates.project import ProjectTemplate
161
+
162
+ # Pass 1: Load YAML with line tracking
163
+ try:
164
+ result = load_yaml_with_line_tracking(yaml_text)
165
+ except YAMLError as e:
166
+ # YAML syntax error - cannot proceed
167
+ validation_info.add_error(
168
+ field_path=ROOT_FIELD_PATH,
169
+ message=f"YAML syntax error: {e}",
170
+ line_number=None,
171
+ )
172
+ return None
173
+
174
+ # Pass 2: Build ProjectTemplate with Pydantic validation
175
+ data = result.data
176
+ line_info = result.line_info
177
+
178
+ # Add names to situations and directories before validation
179
+ if FIELD_SITUATIONS in data and isinstance(data[FIELD_SITUATIONS], dict):
180
+ for sit_name, sit_data in data[FIELD_SITUATIONS].items():
181
+ if isinstance(sit_data, dict):
182
+ sit_data[FIELD_NAME] = sit_name
183
+
184
+ if FIELD_DIRECTORIES in data and isinstance(data[FIELD_DIRECTORIES], dict):
185
+ for dir_name, dir_data in data[FIELD_DIRECTORIES].items():
186
+ if isinstance(dir_data, dict):
187
+ dir_data[FIELD_NAME] = dir_name
188
+
189
+ try:
190
+ template = ProjectTemplate.model_validate(data)
191
+ except ValidationError as e:
192
+ # Convert Pydantic validation errors to our validation_info format
193
+ for error in e.errors():
194
+ # Build field path from error location
195
+ field_path = ".".join(str(loc) for loc in error["loc"])
196
+ message = error["msg"]
197
+
198
+ # Try to get line number from our line tracking
199
+ line_number = line_info.get_line(field_path)
200
+
201
+ validation_info.add_error(
202
+ field_path=field_path,
203
+ message=message,
204
+ line_number=line_number,
205
+ )
206
+
207
+ return None
208
+ else:
209
+ # Add warnings for schema version mismatches
210
+ if template.project_template_schema_version != ProjectTemplate.LATEST_SCHEMA_VERSION:
211
+ validation_info.add_warning(
212
+ field_path=FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION,
213
+ message=f"Schema version '{template.project_template_schema_version}' differs from latest '{ProjectTemplate.LATEST_SCHEMA_VERSION}'",
214
+ line_number=line_info.get_line(FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION),
215
+ )
216
+
217
+ return template
218
+
219
+
220
+ def load_partial_project_template(
221
+ yaml_text: str,
222
+ validation_info: ProjectValidationInfo,
223
+ ) -> ProjectOverlayData | None:
224
+ """Load project template overlay for merging without full construction.
225
+
226
+ Performs minimal structural validation:
227
+ - YAML syntax
228
+ - Required top-level fields (name, project_template_schema_version)
229
+ - Basic type checking (situations is dict, directories is dict, etc.)
230
+
231
+ Does NOT:
232
+ - Construct full SituationTemplate/DirectoryDefinition objects
233
+ - Validate situation schemas or policy values
234
+ - Check directory references
235
+
236
+ Use this for loading user overlay templates before merge.
237
+ After merge, full validation happens during object construction.
238
+
239
+ Args:
240
+ yaml_text: YAML text to parse
241
+ validation_info: Validation info to populate (caller-owned, will be mutated)
242
+
243
+ Returns:
244
+ ProjectOverlayData on success, None if fatal errors prevent construction
245
+ """
246
+ # Parse YAML with line tracking
247
+ try:
248
+ result = load_yaml_with_line_tracking(yaml_text)
249
+ data = result.data
250
+ line_info = result.line_info
251
+ except YAMLError as e:
252
+ validation_info.add_error(
253
+ field_path=ROOT_FIELD_PATH,
254
+ message=f"YAML syntax error: {e}",
255
+ line_number=None,
256
+ )
257
+ return None
258
+
259
+ # Validate required field: name
260
+ name = data.get(FIELD_NAME)
261
+ if name is None:
262
+ validation_info.add_error(
263
+ field_path=FIELD_NAME,
264
+ message=f"Required field '{FIELD_NAME}' missing",
265
+ line_number=line_info.get_line(FIELD_NAME),
266
+ )
267
+ name = DEFAULT_PROJECT_NAME
268
+ elif not isinstance(name, str):
269
+ validation_info.add_error(
270
+ field_path=FIELD_NAME,
271
+ message=f"Field '{FIELD_NAME}' must be string, got {type(name).__name__}",
272
+ line_number=line_info.get_line(FIELD_NAME),
273
+ )
274
+ name = DEFAULT_PROJECT_NAME
275
+
276
+ # Validate required field: project_template_schema_version
277
+ schema_version = data.get(FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION)
278
+ if schema_version is None:
279
+ validation_info.add_error(
280
+ field_path=FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION,
281
+ message=f"Required field '{FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION}' missing",
282
+ line_number=line_info.get_line(FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION),
283
+ )
284
+ schema_version = DEFAULT_SCHEMA_VERSION
285
+ elif not isinstance(schema_version, str):
286
+ validation_info.add_error(
287
+ field_path=FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION,
288
+ message=f"Must be string, got {type(schema_version).__name__}",
289
+ line_number=line_info.get_line(FIELD_PROJECT_TEMPLATE_SCHEMA_VERSION),
290
+ )
291
+ schema_version = DEFAULT_SCHEMA_VERSION
292
+
293
+ # Optional field: situations (default to empty dict)
294
+ situations = data.get(FIELD_SITUATIONS, {})
295
+ if not isinstance(situations, dict):
296
+ validation_info.add_error(
297
+ field_path=FIELD_SITUATIONS,
298
+ message=f"Must be dict, got {type(situations).__name__}",
299
+ line_number=line_info.get_line(FIELD_SITUATIONS),
300
+ )
301
+ situations = {}
302
+
303
+ # Optional field: directories (default to empty dict)
304
+ directories = data.get(FIELD_DIRECTORIES, {})
305
+ if not isinstance(directories, dict):
306
+ validation_info.add_error(
307
+ field_path=FIELD_DIRECTORIES,
308
+ message=f"Must be dict, got {type(directories).__name__}",
309
+ line_number=line_info.get_line(FIELD_DIRECTORIES),
310
+ )
311
+ directories = {}
312
+
313
+ # Optional field: environment (default to empty dict)
314
+ environment = data.get(FIELD_ENVIRONMENT, {})
315
+ if not isinstance(environment, dict):
316
+ validation_info.add_error(
317
+ field_path=FIELD_ENVIRONMENT,
318
+ message=f"Must be dict, got {type(environment).__name__}",
319
+ line_number=line_info.get_line(FIELD_ENVIRONMENT),
320
+ )
321
+ environment = {}
322
+
323
+ # Optional field: description
324
+ description = data.get(FIELD_DESCRIPTION)
325
+ if description is not None and not isinstance(description, str):
326
+ validation_info.add_error(
327
+ field_path=FIELD_DESCRIPTION,
328
+ message=f"Must be string, got {type(description).__name__}",
329
+ line_number=line_info.get_line(FIELD_DESCRIPTION),
330
+ )
331
+ description = None
332
+
333
+ return ProjectOverlayData(
334
+ name=name,
335
+ project_template_schema_version=schema_version,
336
+ situations=situations,
337
+ directories=directories,
338
+ environment=environment,
339
+ description=description,
340
+ line_info=line_info,
341
+ )