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,617 @@
1
+ """ProjectManager - Manages project templates and file save situations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from griptape_nodes.common.macro_parser import (
11
+ MacroMatchFailure,
12
+ MacroMatchFailureReason,
13
+ MacroParseFailure,
14
+ MacroParseFailureReason,
15
+ MacroResolutionError,
16
+ MacroResolutionFailureReason,
17
+ MacroSyntaxError,
18
+ ParsedMacro,
19
+ )
20
+ from griptape_nodes.common.project_templates import (
21
+ DEFAULT_PROJECT_TEMPLATE,
22
+ ProjectTemplate,
23
+ ProjectValidationInfo,
24
+ ProjectValidationStatus,
25
+ load_project_template_from_yaml,
26
+ )
27
+ from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete # noqa: TC001
28
+ from griptape_nodes.retained_mode.events.os_events import ReadFileRequest, ReadFileResultSuccess
29
+ from griptape_nodes.retained_mode.events.project_events import (
30
+ GetAllSituationsForProjectRequest,
31
+ GetAllSituationsForProjectResultFailure,
32
+ GetAllSituationsForProjectResultSuccess,
33
+ GetCurrentProjectRequest,
34
+ GetCurrentProjectResultSuccess,
35
+ GetMacroForSituationRequest,
36
+ GetMacroForSituationResultFailure,
37
+ GetMacroForSituationResultSuccess,
38
+ GetPathForMacroRequest,
39
+ GetPathForMacroResultFailure,
40
+ GetPathForMacroResultSuccess,
41
+ GetProjectTemplateRequest,
42
+ GetProjectTemplateResultFailure,
43
+ GetProjectTemplateResultSuccess,
44
+ GetVariablesForMacroRequest,
45
+ GetVariablesForMacroResultFailure,
46
+ GetVariablesForMacroResultSuccess,
47
+ LoadProjectTemplateRequest,
48
+ LoadProjectTemplateResultFailure,
49
+ LoadProjectTemplateResultSuccess,
50
+ MatchPathAgainstMacroRequest,
51
+ MatchPathAgainstMacroResultFailure,
52
+ MatchPathAgainstMacroResultSuccess,
53
+ PathResolutionFailureReason,
54
+ SaveProjectTemplateRequest,
55
+ SaveProjectTemplateResultFailure,
56
+ SetCurrentProjectRequest,
57
+ SetCurrentProjectResultSuccess,
58
+ ValidateMacroSyntaxRequest,
59
+ ValidateMacroSyntaxResultFailure,
60
+ ValidateMacroSyntaxResultSuccess,
61
+ )
62
+
63
+ if TYPE_CHECKING:
64
+ from griptape_nodes.retained_mode.events.base_events import ResultPayload
65
+ from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
66
+ from griptape_nodes.retained_mode.managers.event_manager import EventManager
67
+ from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
68
+
69
+ logger = logging.getLogger("griptape_nodes")
70
+
71
+ # Synthetic path key for the system default project template
72
+ SYSTEM_DEFAULTS_KEY = Path("<system-defaults>")
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class SituationMacroKey:
77
+ """Key for caching parsed situation schema macros."""
78
+
79
+ project_path: Path
80
+ situation_name: str
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class DirectoryMacroKey:
85
+ """Key for caching parsed directory schema macros."""
86
+
87
+ project_path: Path
88
+ directory_name: str
89
+
90
+
91
+ class ProjectManager:
92
+ """Manages project templates, validation, and file path resolution.
93
+
94
+ Responsibilities:
95
+ - Load and cache project templates (system defaults + user customizations)
96
+ - Track validation status for all load attempts (including MISSING files)
97
+ - Parse and cache macro schemas for performance
98
+ - Resolve file paths using situation templates and variable substitution
99
+ - Manage current project selection
100
+ - Handle project.yml file I/O via OSManager events
101
+
102
+ State tracking uses two dicts:
103
+ - registered_template_status: ALL load attempts (Path -> ProjectValidationInfo)
104
+ - successful_templates: Only usable templates (Path -> ProjectTemplate)
105
+
106
+ This allows UI to query validation status even when template failed to load.
107
+ """
108
+
109
+ def __init__(
110
+ self,
111
+ event_manager: EventManager | None = None,
112
+ config_manager: ConfigManager | None = None,
113
+ secrets_manager: SecretsManager | None = None,
114
+ ) -> None:
115
+ """Initialize the ProjectManager.
116
+
117
+ Args:
118
+ event_manager: The EventManager instance to use for event handling
119
+ config_manager: ConfigManager instance for accessing configuration
120
+ secrets_manager: SecretsManager instance for macro resolution
121
+ """
122
+ self.config_manager = config_manager
123
+ self.secrets_manager = secrets_manager
124
+
125
+ # Track validation status for ALL load attempts (including MISSING/UNUSABLE)
126
+ self.registered_template_status: dict[Path, ProjectValidationInfo] = {}
127
+
128
+ # Cache only successfully loaded templates (GOOD or FLAWED)
129
+ self.successful_templates: dict[Path, ProjectTemplate] = {}
130
+
131
+ # Cache parsed macros for performance (avoid re-parsing schemas)
132
+ self.parsed_situation_schemas: dict[SituationMacroKey, ParsedMacro] = {}
133
+ self.parsed_directory_schemas: dict[DirectoryMacroKey, ParsedMacro] = {}
134
+
135
+ # Track which project.yml user has selected
136
+ self.current_project_path: Path | None = None
137
+
138
+ # Register event handlers
139
+ if event_manager is not None:
140
+ event_manager.assign_manager_to_request_type(
141
+ LoadProjectTemplateRequest, self.on_load_project_template_request
142
+ )
143
+ event_manager.assign_manager_to_request_type(
144
+ GetProjectTemplateRequest, self.on_get_project_template_request
145
+ )
146
+ event_manager.assign_manager_to_request_type(
147
+ GetMacroForSituationRequest, self.on_get_macro_for_situation_request
148
+ )
149
+ event_manager.assign_manager_to_request_type(GetPathForMacroRequest, self.on_get_path_for_macro_request)
150
+ event_manager.assign_manager_to_request_type(SetCurrentProjectRequest, self.on_set_current_project_request)
151
+ event_manager.assign_manager_to_request_type(GetCurrentProjectRequest, self.on_get_current_project_request)
152
+ event_manager.assign_manager_to_request_type(
153
+ SaveProjectTemplateRequest, self.on_save_project_template_request
154
+ )
155
+ event_manager.assign_manager_to_request_type(
156
+ MatchPathAgainstMacroRequest, self.on_match_path_against_macro_request
157
+ )
158
+ event_manager.assign_manager_to_request_type(
159
+ GetVariablesForMacroRequest, self.on_get_variables_for_macro_request
160
+ )
161
+ event_manager.assign_manager_to_request_type(
162
+ ValidateMacroSyntaxRequest, self.on_validate_macro_syntax_request
163
+ )
164
+ event_manager.assign_manager_to_request_type(
165
+ GetAllSituationsForProjectRequest, self.on_get_all_situations_for_project_request
166
+ )
167
+
168
+ # Register app initialization listener
169
+ # NOTE: This is intentionally commented out to keep ProjectManager inert for code review.
170
+ # Uncomment the following lines to enable ProjectManager during app initialization.
171
+ # ruff: noqa: ERA001
172
+ # event_manager.add_listener_to_app_event(
173
+ # AppInitializationComplete,
174
+ # self.on_app_initialization_complete,
175
+ # )
176
+
177
+ # Event handler methods (public)
178
+
179
+ def on_load_project_template_request(self, request: LoadProjectTemplateRequest) -> ResultPayload:
180
+ """Load user's project.yml and merge with system defaults.
181
+
182
+ Flow:
183
+ 1. Issue ReadFileRequest to OSManager (for proper Windows long path handling)
184
+ 2. Parse YAML and load partial template (overlay) using load_partial_project_template()
185
+ 3. Merge with system defaults using ProjectTemplate.merge()
186
+ 4. Cache validation in registered_template_status
187
+ 5. If usable, cache template in successful_templates
188
+ 6. Return LoadProjectTemplateResultSuccess or LoadProjectTemplateResultFailure
189
+ """
190
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
191
+
192
+ logger.debug("Loading project template: %s", request.project_path)
193
+
194
+ read_request = ReadFileRequest(
195
+ file_path=str(request.project_path),
196
+ encoding="utf-8",
197
+ workspace_only=False,
198
+ )
199
+ read_result = GriptapeNodes.handle_request(read_request)
200
+
201
+ if read_result.failed():
202
+ validation = ProjectValidationInfo(status=ProjectValidationStatus.MISSING)
203
+ self.registered_template_status[request.project_path] = validation
204
+
205
+ return LoadProjectTemplateResultFailure(
206
+ project_path=request.project_path,
207
+ validation=validation,
208
+ result_details=f"File not found: {request.project_path}",
209
+ )
210
+
211
+ if not isinstance(read_result, ReadFileResultSuccess):
212
+ validation = ProjectValidationInfo(status=ProjectValidationStatus.UNUSABLE)
213
+ self.registered_template_status[request.project_path] = validation
214
+
215
+ return LoadProjectTemplateResultFailure(
216
+ project_path=request.project_path,
217
+ validation=validation,
218
+ result_details="Unexpected result type from ReadFileRequest",
219
+ )
220
+
221
+ yaml_text = read_result.content
222
+ if not isinstance(yaml_text, str):
223
+ validation = ProjectValidationInfo(status=ProjectValidationStatus.UNUSABLE)
224
+ self.registered_template_status[request.project_path] = validation
225
+
226
+ return LoadProjectTemplateResultFailure(
227
+ project_path=request.project_path,
228
+ validation=validation,
229
+ result_details="Template must be text, got binary content",
230
+ )
231
+
232
+ validation = ProjectValidationInfo(status=ProjectValidationStatus.GOOD)
233
+ template = load_project_template_from_yaml(yaml_text, validation)
234
+
235
+ if template is None:
236
+ self.registered_template_status[request.project_path] = validation
237
+ return LoadProjectTemplateResultFailure(
238
+ project_path=request.project_path,
239
+ validation=validation,
240
+ result_details="Failed to parse YAML template",
241
+ )
242
+
243
+ if not validation.is_usable():
244
+ self.registered_template_status[request.project_path] = validation
245
+ return LoadProjectTemplateResultFailure(
246
+ project_path=request.project_path,
247
+ validation=validation,
248
+ result_details=f"Template not usable (status: {validation.status})",
249
+ )
250
+
251
+ logger.debug("Template loaded successfully (status: %s)", validation.status)
252
+
253
+ self.registered_template_status[request.project_path] = validation
254
+ self.successful_templates[request.project_path] = template
255
+
256
+ return LoadProjectTemplateResultSuccess(
257
+ project_path=request.project_path,
258
+ template=template,
259
+ validation=validation,
260
+ result_details=f"Template loaded successfully with status: {validation.status}",
261
+ )
262
+
263
+ def on_get_project_template_request(self, request: GetProjectTemplateRequest) -> ResultPayload:
264
+ """Get cached template for a workspace path."""
265
+ if request.project_path not in self.registered_template_status:
266
+ return GetProjectTemplateResultFailure(
267
+ result_details=f"Template not loaded yet: {request.project_path}",
268
+ )
269
+
270
+ validation = self.registered_template_status[request.project_path]
271
+ template = self.successful_templates.get(request.project_path)
272
+
273
+ if template is None:
274
+ return GetProjectTemplateResultFailure(
275
+ result_details=f"Template not usable (status: {validation.status})",
276
+ )
277
+
278
+ return GetProjectTemplateResultSuccess(
279
+ template=template,
280
+ validation=validation,
281
+ result_details="Project template retrieved from cache",
282
+ )
283
+
284
+ def on_get_macro_for_situation_request(self, request: GetMacroForSituationRequest) -> ResultPayload:
285
+ """Get the macro schema for a specific situation.
286
+
287
+ Flow:
288
+ 1. Get template from successful_templates
289
+ 2. Get situation from template
290
+ 3. Return situation's macro schema
291
+ """
292
+ logger.debug("Getting macro for situation: %s in project: %s", request.situation_name, request.project_path)
293
+
294
+ template = self.successful_templates.get(request.project_path)
295
+ if template is None:
296
+ return GetMacroForSituationResultFailure(
297
+ result_details=f"Project template not loaded: {request.project_path}",
298
+ )
299
+
300
+ situation = template.situations.get(request.situation_name)
301
+ if situation is None:
302
+ return GetMacroForSituationResultFailure(
303
+ result_details=f"Situation not found: {request.situation_name}",
304
+ )
305
+
306
+ return GetMacroForSituationResultSuccess(
307
+ macro_schema=situation.schema,
308
+ result_details=f"Retrieved schema for situation: {request.situation_name}",
309
+ )
310
+
311
+ def on_get_path_for_macro_request(self, request: GetPathForMacroRequest) -> ResultPayload: # noqa: C901, PLR0911
312
+ """Resolve ANY macro schema with variables to final Path.
313
+
314
+ Flow:
315
+ 1. Parse macro schema with ParsedMacro
316
+ 2. Get variables from ParsedMacro.get_variables()
317
+ 3. For each variable:
318
+ - If in directories dict → resolve directory, add to resolution bag
319
+ - Else if in user_supplied_vars → use user value
320
+ - If in BOTH → ERROR: DIRECTORY_OVERRIDE_ATTEMPTED
321
+ - Else → collect as missing
322
+ 4. If any missing → ERROR: MISSING_REQUIRED_VARIABLES
323
+ 5. Resolve macro with complete variable bag
324
+ 6. Return resolved Path
325
+ """
326
+ logger.debug("Resolving macro: %s in project: %s", request.macro_schema, request.project_path)
327
+
328
+ try:
329
+ parsed_macro = ParsedMacro(request.macro_schema)
330
+ except MacroSyntaxError as e:
331
+ return GetPathForMacroResultFailure(
332
+ failure_reason=PathResolutionFailureReason.MACRO_RESOLUTION_ERROR,
333
+ error_details=str(e),
334
+ result_details=f"Invalid macro syntax: {e}",
335
+ )
336
+
337
+ template = self.successful_templates.get(request.project_path)
338
+ if template is None:
339
+ return GetPathForMacroResultFailure(
340
+ failure_reason=PathResolutionFailureReason.MACRO_RESOLUTION_ERROR,
341
+ error_details="Project template not loaded",
342
+ result_details=f"Project template not loaded: {request.project_path}",
343
+ )
344
+
345
+ variable_infos = parsed_macro.get_variables()
346
+ directory_names = set(template.directories.keys())
347
+ user_provided_names = set(request.variables.keys())
348
+
349
+ conflicting = directory_names & user_provided_names
350
+ if conflicting:
351
+ return GetPathForMacroResultFailure(
352
+ failure_reason=PathResolutionFailureReason.DIRECTORY_OVERRIDE_ATTEMPTED,
353
+ conflicting_variables=sorted(conflicting),
354
+ result_details=f"Variables conflict with directory names: {', '.join(sorted(conflicting))}",
355
+ )
356
+
357
+ resolution_bag: dict[str, str | int] = {}
358
+
359
+ for var_info in variable_infos:
360
+ var_name = var_info.name
361
+
362
+ if var_name in directory_names:
363
+ directory_def = template.directories[var_name]
364
+ resolution_bag[var_name] = directory_def.path_schema
365
+ elif var_name in user_provided_names:
366
+ resolution_bag[var_name] = request.variables[var_name]
367
+
368
+ required_vars = {v.name for v in variable_infos if v.is_required}
369
+ provided_vars = set(resolution_bag.keys())
370
+ missing = required_vars - provided_vars
371
+
372
+ if missing:
373
+ return GetPathForMacroResultFailure(
374
+ failure_reason=PathResolutionFailureReason.MISSING_REQUIRED_VARIABLES,
375
+ missing_variables=sorted(missing),
376
+ result_details=f"Missing required variables: {', '.join(sorted(missing))}",
377
+ )
378
+
379
+ if self.secrets_manager is None:
380
+ return GetPathForMacroResultFailure(
381
+ failure_reason=PathResolutionFailureReason.MACRO_RESOLUTION_ERROR,
382
+ error_details="SecretsManager not available",
383
+ result_details="SecretsManager not available",
384
+ )
385
+
386
+ try:
387
+ resolved_string = parsed_macro.resolve(resolution_bag, self.secrets_manager)
388
+ except MacroResolutionError as e:
389
+ if e.failure_reason == MacroResolutionFailureReason.MISSING_REQUIRED_VARIABLES:
390
+ path_failure_reason = PathResolutionFailureReason.MISSING_REQUIRED_VARIABLES
391
+ else:
392
+ path_failure_reason = PathResolutionFailureReason.MACRO_RESOLUTION_ERROR
393
+
394
+ return GetPathForMacroResultFailure(
395
+ failure_reason=path_failure_reason,
396
+ missing_variables=e.missing_variables,
397
+ error_details=str(e),
398
+ result_details=f"Macro resolution failed: {e}",
399
+ )
400
+
401
+ resolved_path = Path(resolved_string)
402
+
403
+ return GetPathForMacroResultSuccess(
404
+ resolved_path=resolved_path,
405
+ result_details=f"Resolved to: {resolved_path}",
406
+ )
407
+
408
+ def on_set_current_project_request(self, request: SetCurrentProjectRequest) -> ResultPayload:
409
+ """Set which project.yml user has selected."""
410
+ self.current_project_path = request.project_path
411
+
412
+ if request.project_path is None:
413
+ logger.info("Current project set to: No Project")
414
+ else:
415
+ logger.info("Current project set to: %s", request.project_path)
416
+
417
+ return SetCurrentProjectResultSuccess(
418
+ result_details="Current project set successfully",
419
+ )
420
+
421
+ def on_get_current_project_request(self, _request: GetCurrentProjectRequest) -> ResultPayload:
422
+ """Get currently selected project path."""
423
+ return GetCurrentProjectResultSuccess(
424
+ project_path=self.current_project_path,
425
+ result_details="Current project retrieved successfully",
426
+ )
427
+
428
+ def on_save_project_template_request(self, request: SaveProjectTemplateRequest) -> ResultPayload:
429
+ """Save user customizations to project.yml.
430
+
431
+ Flow:
432
+ 1. Convert template_data to YAML format
433
+ 2. Issue WriteFileRequest to OSManager
434
+ 3. Handle write result
435
+ 4. Invalidate cache (force reload on next access)
436
+
437
+ TODO: Implement saving logic when template system merges
438
+ """
439
+ logger.debug("Saving project template: %s", request.project_path)
440
+
441
+ return SaveProjectTemplateResultFailure(
442
+ project_path=request.project_path,
443
+ result_details="Template saving not yet implemented (stub)",
444
+ )
445
+
446
+ def on_match_path_against_macro_request(self, request: MatchPathAgainstMacroRequest) -> ResultPayload:
447
+ """Check if a path matches a macro schema and extract variables.
448
+
449
+ Flow:
450
+ 1. Parse macro schema into ParsedMacro
451
+ 2. Call ParsedMacro.extract_variables() with path and known variables
452
+ 3. If match succeeds, return extracted variables
453
+ 4. If match fails, return MacroMatchFailure with details
454
+ """
455
+ logger.debug("Matching path against macro: %s", request.macro_schema)
456
+
457
+ if self.secrets_manager is None:
458
+ return MatchPathAgainstMacroResultFailure(
459
+ match_failure=MacroMatchFailure(
460
+ failure_reason=MacroMatchFailureReason.INVALID_MACRO_SYNTAX,
461
+ expected_pattern=request.macro_schema,
462
+ known_variables_used=request.known_variables,
463
+ error_details="SecretsManager not available",
464
+ ),
465
+ result_details="SecretsManager not available for macro matching",
466
+ )
467
+
468
+ try:
469
+ parsed_macro = ParsedMacro(request.macro_schema)
470
+ except MacroSyntaxError as err:
471
+ return MatchPathAgainstMacroResultFailure(
472
+ match_failure=MacroMatchFailure(
473
+ failure_reason=MacroMatchFailureReason.INVALID_MACRO_SYNTAX,
474
+ expected_pattern=request.macro_schema,
475
+ known_variables_used=request.known_variables,
476
+ error_details=str(err),
477
+ ),
478
+ result_details=f"Invalid macro syntax: {err}",
479
+ )
480
+
481
+ extracted = parsed_macro.extract_variables(
482
+ request.file_path,
483
+ request.known_variables,
484
+ self.secrets_manager,
485
+ )
486
+
487
+ if extracted is None:
488
+ return MatchPathAgainstMacroResultFailure(
489
+ match_failure=MacroMatchFailure(
490
+ failure_reason=MacroMatchFailureReason.STATIC_TEXT_MISMATCH,
491
+ expected_pattern=request.macro_schema,
492
+ known_variables_used=request.known_variables,
493
+ error_details=f"Path '{request.file_path}' does not match macro pattern",
494
+ ),
495
+ result_details="Path does not match macro pattern",
496
+ )
497
+
498
+ return MatchPathAgainstMacroResultSuccess(
499
+ extracted_variables=extracted,
500
+ result_details="Successfully matched path against macro",
501
+ )
502
+
503
+ def on_get_variables_for_macro_request(self, request: GetVariablesForMacroRequest) -> ResultPayload:
504
+ """Get list of all variables in a macro schema.
505
+
506
+ Flow:
507
+ 1. Parse macro schema into ParsedMacro
508
+ 2. Call ParsedMacro.get_variables() to extract variable metadata
509
+ 3. Return list of VariableInfo
510
+ """
511
+ logger.debug("Getting variables for macro: %s", request.macro_schema)
512
+
513
+ try:
514
+ parsed_macro = ParsedMacro(request.macro_schema)
515
+ except MacroSyntaxError as err:
516
+ return GetVariablesForMacroResultFailure(
517
+ parse_failure=MacroParseFailure(
518
+ failure_reason=err.failure_reason or MacroParseFailureReason.UNEXPECTED_SEGMENT_TYPE,
519
+ error_position=err.error_position,
520
+ error_details=str(err),
521
+ ),
522
+ result_details=f"Failed to parse macro: {err}",
523
+ )
524
+
525
+ variables = parsed_macro.get_variables()
526
+
527
+ return GetVariablesForMacroResultSuccess(
528
+ variables=variables,
529
+ result_details=f"Found {len(variables)} variables in macro",
530
+ )
531
+
532
+ def on_validate_macro_syntax_request(self, request: ValidateMacroSyntaxRequest) -> ResultPayload:
533
+ """Validate a macro schema string for syntax errors.
534
+
535
+ Flow:
536
+ 1. Try to parse macro schema with ParsedMacro()
537
+ 2. If successful, return variables found and any warnings
538
+ 3. If syntax error, return MacroParseFailure with details
539
+ """
540
+ logger.debug("Validating macro syntax: %s", request.macro_schema)
541
+
542
+ try:
543
+ parsed_macro = ParsedMacro(request.macro_schema)
544
+ except MacroSyntaxError as err:
545
+ return ValidateMacroSyntaxResultFailure(
546
+ parse_failure=MacroParseFailure(
547
+ failure_reason=err.failure_reason or MacroParseFailureReason.UNEXPECTED_SEGMENT_TYPE,
548
+ error_position=err.error_position,
549
+ error_details=str(err),
550
+ ),
551
+ partial_variables=[],
552
+ result_details=f"Syntax validation failed: {err}",
553
+ )
554
+
555
+ variables = parsed_macro.get_variables()
556
+
557
+ return ValidateMacroSyntaxResultSuccess(
558
+ variables=variables,
559
+ warnings=[],
560
+ result_details=f"Macro syntax is valid with {len(variables)} variables",
561
+ )
562
+
563
+ async def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
564
+ """Load system default project template when app initializes.
565
+
566
+ Called by EventManager after all libraries are loaded.
567
+ """
568
+ logger.debug("ProjectManager: Loading system default project template")
569
+
570
+ self._load_system_defaults()
571
+
572
+ # Set as current project (using synthetic key for system defaults)
573
+ set_request = SetCurrentProjectRequest(project_path=SYSTEM_DEFAULTS_KEY)
574
+ result = self.on_set_current_project_request(set_request)
575
+
576
+ if result.failed():
577
+ logger.error("Failed to set default project as current: %s", result.result_details)
578
+ else:
579
+ logger.debug("Successfully loaded default project template")
580
+
581
+ def on_get_all_situations_for_project_request(self, request: GetAllSituationsForProjectRequest) -> ResultPayload:
582
+ """Get all situation names and schemas from a project template."""
583
+ logger.debug("Getting all situations for project: %s", request.project_path)
584
+
585
+ template_info = self.registered_template_status.get(request.project_path)
586
+
587
+ if template_info is None:
588
+ self._load_system_defaults()
589
+ template_info = self.registered_template_status.get(request.project_path)
590
+
591
+ if template_info is None or template_info.status != ProjectValidationStatus.GOOD:
592
+ return GetAllSituationsForProjectResultFailure(result_details="Project template not available or invalid")
593
+
594
+ template = self.successful_templates[request.project_path]
595
+ situations = {situation_name: situation.schema for situation_name, situation in template.situations.items()}
596
+
597
+ return GetAllSituationsForProjectResultSuccess(
598
+ situations=situations, result_details=f"Found {len(situations)} situations"
599
+ )
600
+
601
+ # Private helper methods
602
+
603
+ def _load_system_defaults(self) -> None:
604
+ """Load bundled system default template.
605
+
606
+ System defaults are now defined in Python as DEFAULT_PROJECT_TEMPLATE.
607
+ This is always valid by construction.
608
+ """
609
+ logger.debug("Loading system default template")
610
+
611
+ # Create validation info to track that defaults were loaded
612
+ validation = ProjectValidationInfo(status=ProjectValidationStatus.GOOD)
613
+
614
+ logger.debug("System defaults loaded successfully")
615
+
616
+ self.registered_template_status[SYSTEM_DEFAULTS_KEY] = validation
617
+ self.successful_templates[SYSTEM_DEFAULTS_KEY] = DEFAULT_PROJECT_TEMPLATE
@@ -24,6 +24,7 @@ EXECUTION = Category(name="Execution", description="Workflow execution and proce
24
24
  STORAGE = Category(name="Storage", description="Data storage and persistence configuration")
25
25
  SYSTEM_REQUIREMENTS = Category(name="System Requirements", description="System resource requirements and limits")
26
26
  MCP_SERVERS = Category(name="MCP Servers", description="Model Context Protocol server configurations")
27
+ PROJECTS = Category(name="Projects", description="Project template configurations and registrations")
27
28
 
28
29
 
29
30
  def Field(category: str | Category = "General", **kwargs) -> Any:
@@ -99,6 +100,11 @@ class AppInitializationComplete(BaseModel):
99
100
  description="Core secrets to register in the secrets manager. Library-specific secrets are registered automatically from library settings.",
100
101
  )
101
102
  models_to_download: list[str] = Field(default_factory=list)
103
+ projects_to_register: list[str] = Field(
104
+ category=PROJECTS,
105
+ default_factory=list,
106
+ description="List of project.yml file paths to load at startup",
107
+ )
102
108
 
103
109
 
104
110
  class AppEvents(BaseModel):