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.
- 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 +346 -1
- 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 +17 -71
- griptape_nodes/traits/button.py +18 -0
- {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/METADATA +5 -3
- {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/RECORD +47 -31
- {griptape_nodes-0.60.3.dist-info → griptape_nodes-0.61.0.dist-info}/WHEEL +1 -1
- {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):
|