griptape-nodes 0.53.0__py3-none-any.whl → 0.54.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 (56) hide show
  1. griptape_nodes/__init__.py +5 -2
  2. griptape_nodes/app/app.py +4 -26
  3. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +35 -5
  4. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +15 -1
  5. griptape_nodes/cli/commands/config.py +4 -1
  6. griptape_nodes/cli/commands/init.py +5 -3
  7. griptape_nodes/cli/commands/libraries.py +14 -8
  8. griptape_nodes/cli/commands/models.py +504 -0
  9. griptape_nodes/cli/commands/self.py +5 -2
  10. griptape_nodes/cli/main.py +11 -1
  11. griptape_nodes/cli/shared.py +0 -9
  12. griptape_nodes/common/directed_graph.py +17 -1
  13. griptape_nodes/drivers/storage/base_storage_driver.py +40 -20
  14. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +24 -29
  15. griptape_nodes/drivers/storage/local_storage_driver.py +17 -13
  16. griptape_nodes/exe_types/node_types.py +219 -14
  17. griptape_nodes/exe_types/param_components/__init__.py +1 -0
  18. griptape_nodes/exe_types/param_components/execution_status_component.py +138 -0
  19. griptape_nodes/machines/control_flow.py +129 -92
  20. griptape_nodes/machines/dag_builder.py +207 -0
  21. griptape_nodes/machines/parallel_resolution.py +264 -276
  22. griptape_nodes/machines/sequential_resolution.py +9 -7
  23. griptape_nodes/node_library/library_registry.py +34 -1
  24. griptape_nodes/retained_mode/events/app_events.py +5 -1
  25. griptape_nodes/retained_mode/events/base_events.py +7 -7
  26. griptape_nodes/retained_mode/events/config_events.py +30 -0
  27. griptape_nodes/retained_mode/events/execution_events.py +2 -2
  28. griptape_nodes/retained_mode/events/model_events.py +296 -0
  29. griptape_nodes/retained_mode/griptape_nodes.py +10 -1
  30. griptape_nodes/retained_mode/managers/agent_manager.py +14 -0
  31. griptape_nodes/retained_mode/managers/config_manager.py +44 -3
  32. griptape_nodes/retained_mode/managers/event_manager.py +8 -2
  33. griptape_nodes/retained_mode/managers/flow_manager.py +45 -14
  34. griptape_nodes/retained_mode/managers/library_manager.py +3 -3
  35. griptape_nodes/retained_mode/managers/model_manager.py +1107 -0
  36. griptape_nodes/retained_mode/managers/node_manager.py +26 -26
  37. griptape_nodes/retained_mode/managers/object_manager.py +1 -1
  38. griptape_nodes/retained_mode/managers/os_manager.py +6 -6
  39. griptape_nodes/retained_mode/managers/settings.py +87 -9
  40. griptape_nodes/retained_mode/managers/static_files_manager.py +77 -9
  41. griptape_nodes/retained_mode/managers/sync_manager.py +10 -5
  42. griptape_nodes/retained_mode/managers/workflow_manager.py +98 -92
  43. griptape_nodes/retained_mode/retained_mode.py +19 -0
  44. griptape_nodes/servers/__init__.py +1 -0
  45. griptape_nodes/{mcp_server/server.py → servers/mcp.py} +1 -1
  46. griptape_nodes/{app/api.py → servers/static.py} +43 -40
  47. griptape_nodes/traits/button.py +124 -6
  48. griptape_nodes/traits/multi_options.py +188 -0
  49. griptape_nodes/traits/numbers_selector.py +77 -0
  50. griptape_nodes/traits/options.py +93 -2
  51. griptape_nodes/utils/async_utils.py +31 -0
  52. {griptape_nodes-0.53.0.dist-info → griptape_nodes-0.54.0.dist-info}/METADATA +3 -1
  53. {griptape_nodes-0.53.0.dist-info → griptape_nodes-0.54.0.dist-info}/RECORD +56 -47
  54. {griptape_nodes-0.53.0.dist-info → griptape_nodes-0.54.0.dist-info}/WHEEL +1 -1
  55. /griptape_nodes/{mcp_server → servers}/ws_request_manager.py +0 -0
  56. {griptape_nodes-0.53.0.dist-info → griptape_nodes-0.54.0.dist-info}/entry_points.txt +0 -0
@@ -63,12 +63,6 @@ class InitializeSpotlightState(State):
63
63
  @staticmethod
64
64
  async def on_enter(context: ResolutionContext) -> type[State] | None:
65
65
  # If the focus stack is empty
66
- current_node = context.current_node
67
- GriptapeNodes.EventManager().put_event(
68
- ExecutionGriptapeNodeEvent(
69
- wrapped_event=ExecutionEvent(payload=CurrentDataNodeEvent(node_name=current_node.name))
70
- )
71
- )
72
66
  if not context.paused:
73
67
  return InitializeSpotlightState
74
68
  return None
@@ -207,6 +201,11 @@ class ExecuteNodeState(State):
207
201
 
208
202
  # Clear all of the current output values
209
203
  # if node is locked, don't clear anything. skip all of this.
204
+ GriptapeNodes.EventManager().put_event(
205
+ ExecutionGriptapeNodeEvent(
206
+ wrapped_event=ExecutionEvent(payload=CurrentDataNodeEvent(node_name=current_node.name))
207
+ )
208
+ )
210
209
  if current_node.lock:
211
210
  return ExecuteNodeState
212
211
  await ExecuteNodeState.collect_values_from_upstream_nodes(context)
@@ -375,7 +374,10 @@ class SequentialResolutionMachine(FSM[ResolutionContext]):
375
374
  resolution_context = ResolutionContext()
376
375
  super().__init__(resolution_context)
377
376
 
378
- async def resolve_node(self, node: BaseNode) -> None:
377
+ async def resolve_node(self, node: BaseNode | None = None) -> None:
378
+ if node is None:
379
+ msg = "SequentialResolutionMachine requires a node to resolve"
380
+ raise ValueError(msg)
379
381
  self._context.focus_stack.append(Focus(node=node))
380
382
  await self.start(InitializeSpotlightState)
381
383
 
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import logging
4
4
  from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple
5
5
 
6
- from pydantic import BaseModel
6
+ from pydantic import BaseModel, Field
7
7
 
8
8
  from griptape_nodes.utils.metaclasses import SingletonMeta
9
9
 
@@ -85,6 +85,9 @@ class Setting(BaseModel):
85
85
  category: str # Name of the category in the config
86
86
  contents: dict[str, Any] # The actual settings content
87
87
  description: str | None = None # Optional description for the setting
88
+ json_schema: dict[str, Any] | None = Field(
89
+ default=None, alias="schema"
90
+ ) # JSON schema for the setting (including enums)
88
91
 
89
92
 
90
93
  class LibrarySchema(BaseModel):
@@ -230,6 +233,36 @@ class LibraryRegistry(metaclass=SingletonMeta):
230
233
  # Ask the library to create the node.
231
234
  return dest_library.create_node(node_type=node_type, name=name, metadata=metadata)
232
235
 
236
+ @classmethod
237
+ def get_all_library_schemas(cls) -> dict[str, dict]:
238
+ """Get schemas from all loaded libraries.
239
+
240
+ Returns:
241
+ Dictionary mapping category names to their JSON Schema dicts
242
+ """
243
+ instance = cls()
244
+ schemas = {}
245
+
246
+ # Get explicit schemas from loaded libraries
247
+ for library in instance._libraries.values():
248
+ library_data = library.get_library_data()
249
+ if library_data.settings:
250
+ for setting in library_data.settings:
251
+ if setting.json_schema:
252
+ schemas[setting.category] = {
253
+ "type": "object",
254
+ "properties": setting.json_schema,
255
+ "title": setting.description or f"{setting.category.title()} Settings",
256
+ }
257
+ else:
258
+ # Create fallback schema for settings without explicit schemas
259
+ schemas[setting.category] = {
260
+ "type": "object",
261
+ "title": setting.description or f"{setting.category.title()} Settings",
262
+ }
263
+
264
+ return schemas
265
+
233
266
 
234
267
  class Library:
235
268
  """A collection of nodes curated by library author.
@@ -1,4 +1,4 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
2
 
3
3
  from griptape_nodes.retained_mode.events.base_events import (
4
4
  AppPayload,
@@ -76,6 +76,10 @@ class AppGetSessionResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
76
76
  class AppInitializationComplete(AppPayload):
77
77
  """Application initialization completed successfully. All subsystems ready."""
78
78
 
79
+ libraries_to_register: list[str] = field(default_factory=list)
80
+ workflows_to_register: list[str] = field(default_factory=list)
81
+ models_to_download: list[str] = field(default_factory=list)
82
+
79
83
 
80
84
  @dataclass
81
85
  @PayloadRegistry.register
@@ -4,7 +4,7 @@ import json
4
4
  import logging
5
5
  from abc import ABC, abstractmethod
6
6
  from dataclasses import asdict, dataclass, field, is_dataclass
7
- from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar
7
+ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
8
8
 
9
9
  from griptape.artifacts import BaseArtifact
10
10
  from griptape.mixins.serializable_mixin import SerializableMixin
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
20
20
  class ResultDetail:
21
21
  """A single detail about an operation result, including logging level and human readable message."""
22
22
 
23
- level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
23
+ level: int
24
24
  message: str
25
25
 
26
26
 
@@ -34,7 +34,7 @@ class ResultDetails:
34
34
  self,
35
35
  *result_details: ResultDetail,
36
36
  message: str | None = None,
37
- level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None,
37
+ level: int | None = None,
38
38
  logger: logging.Logger | str | None = "griptape_nodes",
39
39
  ):
40
40
  """Initialize with ResultDetail objects or create a single one from message/level.
@@ -67,8 +67,8 @@ class ResultDetails:
67
67
  logger = logging.getLogger(logger)
68
68
 
69
69
  for detail in self.result_details:
70
- numeric_level = getattr(logging, detail.level)
71
- logger.log(numeric_level, detail.message)
70
+ # Handle both string and int levels
71
+ logger.log(detail.level, detail.message)
72
72
  except Exception: # noqa: S110
73
73
  # If logging fails for any reason, don't let it break the ResultDetails creation
74
74
  pass
@@ -140,7 +140,7 @@ class ResultPayloadSuccess(ResultPayload, ABC):
140
140
  def __post_init__(self) -> None:
141
141
  """Initialize success result with INFO level default for strings."""
142
142
  if isinstance(self.result_details, str):
143
- self.result_details = ResultDetails(message=self.result_details, level="DEBUG")
143
+ self.result_details = ResultDetails(message=self.result_details, level=logging.DEBUG)
144
144
 
145
145
  def succeeded(self) -> bool:
146
146
  """Returns True as this is a success result.
@@ -162,7 +162,7 @@ class ResultPayloadFailure(ResultPayload, ABC):
162
162
  def __post_init__(self) -> None:
163
163
  """Initialize failure result with ERROR level default for strings."""
164
164
  if isinstance(self.result_details, str):
165
- self.result_details = ResultDetails(message=self.result_details, level="ERROR")
165
+ self.result_details = ResultDetails(message=self.result_details, level=logging.ERROR)
166
166
 
167
167
  def succeeded(self) -> bool:
168
168
  """Returns False as this is a failure result.
@@ -194,3 +194,33 @@ class ResetConfigResultSuccess(ResultPayloadSuccess):
194
194
  @PayloadRegistry.register
195
195
  class ResetConfigResultFailure(ResultPayloadFailure):
196
196
  """Configuration reset failed. Common causes: file system errors, permission issues, initialization errors."""
197
+
198
+
199
+ @dataclass
200
+ @PayloadRegistry.register
201
+ class GetConfigSchemaRequest(RequestPayload):
202
+ """Get the JSON schema for the configuration model.
203
+
204
+ Use when: Frontend needs to understand field types, enums, and validation rules
205
+ for rendering appropriate UI components (dropdowns, text inputs, etc.).
206
+
207
+ Results: GetConfigSchemaResultSuccess (with schema) | GetConfigSchemaResultFailure (schema generation error)
208
+ """
209
+
210
+
211
+ @dataclass
212
+ @PayloadRegistry.register
213
+ class GetConfigSchemaResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
214
+ """Configuration schema retrieved successfully.
215
+
216
+ Args:
217
+ schema: The JSON schema for the configuration model
218
+ """
219
+
220
+ schema: dict[str, Any]
221
+
222
+
223
+ @dataclass
224
+ @PayloadRegistry.register
225
+ class GetConfigSchemaResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
226
+ """Configuration schema retrieval failed. Common causes: schema generation error, model validation issues."""
@@ -227,8 +227,8 @@ class GetFlowStateResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
227
227
  resolving_node: Name of the node currently being resolved (if any)
228
228
  """
229
229
 
230
- control_node: str | None
231
- resolving_node: str | None
230
+ control_nodes: list[str] | None
231
+ resolving_node: list[str] | None
232
232
 
233
233
 
234
234
  @dataclass
@@ -0,0 +1,296 @@
1
+ from dataclasses import dataclass
2
+
3
+ from griptape_nodes.retained_mode.events.base_events import (
4
+ RequestPayload,
5
+ ResultPayloadFailure,
6
+ ResultPayloadSuccess,
7
+ WorkflowNotAlteredMixin,
8
+ )
9
+ from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
10
+
11
+
12
+ @dataclass
13
+ @PayloadRegistry.register
14
+ class DownloadModelRequest(RequestPayload):
15
+ """Download a model from Hugging Face Hub.
16
+
17
+ Use when: Downloading models for local inference, caching models for offline use,
18
+ retrieving specific model versions or files from Hugging Face repositories.
19
+
20
+ Args:
21
+ model_id: Model identifier (e.g., "microsoft/DialoGPT-medium") or full URL to Hugging Face model
22
+ local_dir: Optional local directory to download the model to (defaults to Hugging Face cache)
23
+ repo_type: Type of repository ("model", "dataset", or "space"). Defaults to "model"
24
+ revision: Git revision (branch, tag, or commit hash) to download. Defaults to "main"
25
+ allow_patterns: List of glob patterns to include when downloading. None means all files
26
+ ignore_patterns: List of glob patterns to exclude when downloading
27
+
28
+ Results: DownloadModelResultSuccess (with local_path) | DownloadModelResultFailure (download error)
29
+ """
30
+
31
+ model_id: str
32
+ local_dir: str | None = None
33
+ repo_type: str = "model"
34
+ revision: str = "main"
35
+ allow_patterns: list[str] | None = None
36
+ ignore_patterns: list[str] | None = None
37
+
38
+
39
+ @dataclass
40
+ @PayloadRegistry.register
41
+ class DownloadModelResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
42
+ """Model download completed successfully.
43
+
44
+ Args:
45
+ model_id: The model ID that was downloaded
46
+ repo_info: Additional repository information returned from the download
47
+ """
48
+
49
+ model_id: str
50
+
51
+
52
+ @dataclass
53
+ @PayloadRegistry.register
54
+ class DownloadModelResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
55
+ """Model download failed. Common causes: invalid model ID, network error, authentication required, storage full."""
56
+
57
+
58
+ @dataclass
59
+ class ModelInfo:
60
+ """Information about a model."""
61
+
62
+ model_id: str
63
+ local_path: str | None = None
64
+ size_bytes: int | None = None
65
+ author: str | None = None
66
+ downloads: int | None = None
67
+ likes: int | None = None
68
+ created_at: str | None = None
69
+ updated_at: str | None = None
70
+ task: str | None = None
71
+ library: str | None = None
72
+ tags: list[str] | None = None
73
+
74
+
75
+ @dataclass
76
+ class QueryInfo:
77
+ """Information about a search query."""
78
+
79
+ query: str | None = None
80
+ task: str | None = None
81
+ library: str | None = None
82
+ author: str | None = None
83
+ tags: list[str] | None = None
84
+ limit: int = 20
85
+ sort: str = "downloads"
86
+ direction: str = "desc"
87
+
88
+
89
+ @dataclass
90
+ @PayloadRegistry.register
91
+ class ListModelsRequest(RequestPayload):
92
+ """List all downloaded models from the local cache.
93
+
94
+ Use when: Viewing what models are available locally, checking cache usage,
95
+ managing local model storage.
96
+
97
+ Results: ListModelsResultSuccess (with model list) | ListModelsResultFailure (listing error)
98
+ """
99
+
100
+
101
+ @dataclass
102
+ @PayloadRegistry.register
103
+ class ListModelsResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
104
+ """Model listing completed successfully.
105
+
106
+ Args:
107
+ models: List of model information containing model_id, local_path, size_bytes, etc.
108
+ """
109
+
110
+ models: list[ModelInfo]
111
+
112
+
113
+ @dataclass
114
+ @PayloadRegistry.register
115
+ class ListModelsResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
116
+ """Model listing failed. Common causes: cache directory access error, filesystem error."""
117
+
118
+
119
+ @dataclass
120
+ @PayloadRegistry.register
121
+ class DeleteModelRequest(RequestPayload):
122
+ """Delete a downloaded model from the local cache.
123
+
124
+ Use when: Cleaning up disk space, removing unused models, managing local storage.
125
+
126
+ Args:
127
+ model_id: Model identifier to delete from local cache
128
+
129
+ Results: DeleteModelResultSuccess (deletion confirmed) | DeleteModelResultFailure (deletion error)
130
+ """
131
+
132
+ model_id: str
133
+
134
+
135
+ @dataclass
136
+ @PayloadRegistry.register
137
+ class DeleteModelResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
138
+ """Model deletion completed successfully.
139
+
140
+ Args:
141
+ model_id: The model ID that was deleted
142
+ deleted_path: Local path that was removed
143
+ """
144
+
145
+ model_id: str
146
+ deleted_path: str
147
+
148
+
149
+ @dataclass
150
+ @PayloadRegistry.register
151
+ class DeleteModelResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
152
+ """Model deletion failed. Common causes: model not found, filesystem error, permission denied."""
153
+
154
+
155
+ @dataclass
156
+ @PayloadRegistry.register
157
+ class ListModelDownloadsRequest(RequestPayload):
158
+ """List download status for a specific model or all downloads.
159
+
160
+ Use when: Checking progress of ongoing downloads, viewing download history,
161
+ monitoring download completion.
162
+
163
+ Args:
164
+ model_id: Optional model identifier to get status for. If None, returns all downloads.
165
+
166
+ Results: ListModelDownloadsResultSuccess (with status data) | ListModelDownloadsResultFailure (query error)
167
+ """
168
+
169
+ model_id: str | None = None
170
+
171
+
172
+ @dataclass
173
+ class ModelDownloadStatus:
174
+ """Model download status tracking multiple files."""
175
+
176
+ model_id: str
177
+ status: str # "downloading", "completed", "failed"
178
+ started_at: str
179
+ updated_at: str
180
+ total_files: int | None = None
181
+ completed_files: int | None = None
182
+ failed_files: int | None = None
183
+ # Optional fields for completed downloads
184
+ completed_at: str | None = None
185
+ local_path: str | None = None
186
+ # Optional fields for failed downloads
187
+ failed_at: str | None = None
188
+ error_message: str | None = None
189
+
190
+
191
+ @dataclass
192
+ @PayloadRegistry.register
193
+ class ListModelDownloadsResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
194
+ """Model download status retrieved successfully.
195
+
196
+ Args:
197
+ downloads: List of download status records or single status if model_id was specified
198
+ """
199
+
200
+ downloads: list[ModelDownloadStatus]
201
+
202
+
203
+ @dataclass
204
+ @PayloadRegistry.register
205
+ class ListModelDownloadsResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
206
+ """Model download status query failed. Common causes: filesystem error, invalid model ID."""
207
+
208
+
209
+ @dataclass
210
+ @PayloadRegistry.register
211
+ class DeleteModelDownloadRequest(RequestPayload):
212
+ """Delete download status tracking records for a model.
213
+
214
+ Use when: Cleaning up orphaned download status files, removing tracking data
215
+ for models that are no longer needed.
216
+
217
+ Args:
218
+ model_id: Model identifier to remove download status for
219
+
220
+ Results: DeleteModelDownloadResultSuccess (deletion confirmed) | DeleteModelDownloadResultFailure (deletion error)
221
+ """
222
+
223
+ model_id: str
224
+
225
+
226
+ @dataclass
227
+ @PayloadRegistry.register
228
+ class DeleteModelDownloadResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
229
+ """Model download status deletion completed successfully.
230
+
231
+ Args:
232
+ model_id: The model ID whose download status was deleted
233
+ deleted_path: Path to the status file that was removed
234
+ """
235
+
236
+ model_id: str
237
+ deleted_path: str
238
+
239
+
240
+ @dataclass
241
+ @PayloadRegistry.register
242
+ class DeleteModelDownloadResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
243
+ """Model download status deletion failed. Common causes: status not found, filesystem error, permission denied."""
244
+
245
+
246
+ @dataclass
247
+ @PayloadRegistry.register
248
+ class SearchModelsRequest(RequestPayload):
249
+ """Search for models on Hugging Face Hub.
250
+
251
+ Use when: Finding models by name, filtering models by task or library,
252
+ discovering available models for specific use cases.
253
+
254
+ Args:
255
+ query: Search query string to match against model names and descriptions
256
+ task: Filter by task type (e.g., "text-generation", "image-classification")
257
+ library: Filter by library (e.g., "transformers", "diffusers", "timm")
258
+ author: Filter by author/organization name
259
+ tags: List of tags to filter by
260
+ limit: Maximum number of results to return (default: 20, max: 100)
261
+ sort: Sort results by "downloads", "likes", "updated", or "created" (default: "downloads")
262
+ direction: Sort direction "asc" or "desc" (default: "desc")
263
+
264
+ Results: SearchModelsResultSuccess (with model list) | SearchModelsResultFailure (search error)
265
+ """
266
+
267
+ query: str | None = None
268
+ task: str | None = None
269
+ library: str | None = None
270
+ author: str | None = None
271
+ tags: list[str] | None = None
272
+ limit: int = 20
273
+ sort: str = "downloads"
274
+ direction: str = "desc"
275
+
276
+
277
+ @dataclass
278
+ @PayloadRegistry.register
279
+ class SearchModelsResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
280
+ """Model search completed successfully.
281
+
282
+ Args:
283
+ models: List of model information containing id, author, downloads, etc.
284
+ total_results: Total number of models matching the search criteria
285
+ query_info: Information about the search query parameters used
286
+ """
287
+
288
+ models: list[ModelInfo]
289
+ total_results: int
290
+ query_info: QueryInfo
291
+
292
+
293
+ @dataclass
294
+ @PayloadRegistry.register
295
+ class SearchModelsResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
296
+ """Model search failed. Common causes: network error, invalid parameters, API limits."""
@@ -62,6 +62,7 @@ if TYPE_CHECKING:
62
62
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
63
63
  from griptape_nodes.retained_mode.managers.flow_manager import FlowManager
64
64
  from griptape_nodes.retained_mode.managers.library_manager import LibraryManager
65
+ from griptape_nodes.retained_mode.managers.model_manager import ModelManager
65
66
  from griptape_nodes.retained_mode.managers.node_manager import NodeManager
66
67
  from griptape_nodes.retained_mode.managers.object_manager import ObjectManager
67
68
  from griptape_nodes.retained_mode.managers.operation_manager import (
@@ -138,6 +139,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
138
139
  _flow_manager: FlowManager
139
140
  _context_manager: ContextManager
140
141
  _library_manager: LibraryManager
142
+ _model_manager: ModelManager
141
143
  _workflow_manager: WorkflowManager
142
144
  _workflow_variables_manager: VariablesManager
143
145
  _arbitrary_code_exec_manager: ArbitraryCodeExecManager
@@ -160,6 +162,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
160
162
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
161
163
  from griptape_nodes.retained_mode.managers.flow_manager import FlowManager
162
164
  from griptape_nodes.retained_mode.managers.library_manager import LibraryManager
165
+ from griptape_nodes.retained_mode.managers.model_manager import ModelManager
163
166
  from griptape_nodes.retained_mode.managers.node_manager import NodeManager
164
167
  from griptape_nodes.retained_mode.managers.object_manager import ObjectManager
165
168
  from griptape_nodes.retained_mode.managers.operation_manager import (
@@ -193,6 +196,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
193
196
  self._flow_manager = FlowManager(self._event_manager)
194
197
  self._context_manager = ContextManager(self._event_manager)
195
198
  self._library_manager = LibraryManager(self._event_manager)
199
+ self._model_manager = ModelManager(self._event_manager)
196
200
  self._workflow_manager = WorkflowManager(self._event_manager)
197
201
  self._workflow_variables_manager = VariablesManager(self._event_manager)
198
202
  self._arbitrary_code_exec_manager = ArbitraryCodeExecManager(self._event_manager)
@@ -306,6 +310,10 @@ class GriptapeNodes(metaclass=SingletonMeta):
306
310
  def LibraryManager(cls) -> LibraryManager:
307
311
  return GriptapeNodes.get_instance()._library_manager
308
312
 
313
+ @classmethod
314
+ def ModelManager(cls) -> ModelManager:
315
+ return GriptapeNodes.get_instance()._model_manager
316
+
309
317
  @classmethod
310
318
  def ObjectManager(cls) -> ObjectManager:
311
319
  return GriptapeNodes.get_instance()._object_manager
@@ -560,7 +568,8 @@ class GriptapeNodes(metaclass=SingletonMeta):
560
568
  GriptapeNodes.EngineIdentityManager().set_engine_name(request.engine_name.strip())
561
569
  details = f"Engine name set to: {request.engine_name.strip()}"
562
570
  return SetEngineNameResultSuccess(
563
- engine_name=request.engine_name.strip(), result_details=ResultDetails(message=details, level="INFO")
571
+ engine_name=request.engine_name.strip(),
572
+ result_details=ResultDetails(message=details, level=logging.INFO),
564
573
  )
565
574
 
566
575
  except Exception as err:
@@ -2,6 +2,7 @@ import asyncio
2
2
  import json
3
3
  import logging
4
4
  import os
5
+ import threading
5
6
  import uuid
6
7
  from typing import TYPE_CHECKING
7
8
 
@@ -37,6 +38,7 @@ from griptape_nodes.retained_mode.events.agent_events import (
37
38
  RunAgentResultStarted,
38
39
  RunAgentResultSuccess,
39
40
  )
41
+ from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
40
42
  from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, ExecutionGriptapeNodeEvent, ResultPayload
41
43
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
42
44
  from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
@@ -45,6 +47,7 @@ from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
45
47
  from griptape_nodes.retained_mode.managers.static_files_manager import (
46
48
  StaticFilesManager,
47
49
  )
50
+ from griptape_nodes.servers.mcp import start_mcp_server
48
51
 
49
52
  if TYPE_CHECKING:
50
53
  from griptape.tools.mcp.sessions import StreamableHttpConnection
@@ -104,6 +107,11 @@ class AgentManager:
104
107
  event_manager.assign_manager_to_request_type(
105
108
  GetConversationMemoryRequest, self.on_handle_get_conversation_memory_request
106
109
  )
110
+ event_manager.add_listener_to_app_event(
111
+ AppInitializationComplete,
112
+ self.on_app_initialization_complete,
113
+ )
114
+ # TODO: Listen for shutdown event (https://github.com/griptape-ai/griptape-nodes/issues/2149) to stop mcp server
107
115
 
108
116
  def _initialize_prompt_driver(self) -> GriptapeCloudPromptDriver:
109
117
  api_key = secrets_manager.get_secret(API_KEY_ENV_VAR)
@@ -251,3 +259,9 @@ class AgentManager:
251
259
  return GetConversationMemoryResultSuccess(
252
260
  runs=conversation_memory, result_details="Conversation memory retrieved successfully."
253
261
  )
262
+
263
+ def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
264
+ secrets_manager = GriptapeNodes.SecretsManager()
265
+ api_key = secrets_manager.get_secret("GT_CLOUD_API_KEY")
266
+ # Start MCP server in daemon thread
267
+ threading.Thread(target=start_mcp_server, args=(api_key,), daemon=True, name="mcp-server").start()
@@ -8,16 +8,18 @@ from typing import Any, Literal
8
8
  from pydantic import ValidationError
9
9
  from xdg_base_dirs import xdg_config_home
10
10
 
11
+ from griptape_nodes.node_library.library_registry import LibraryRegistry
11
12
  from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
12
- from griptape_nodes.retained_mode.events.base_events import (
13
- ResultPayload,
14
- )
13
+ from griptape_nodes.retained_mode.events.base_events import ResultPayload
15
14
  from griptape_nodes.retained_mode.events.config_events import (
16
15
  GetConfigCategoryRequest,
17
16
  GetConfigCategoryResultFailure,
18
17
  GetConfigCategoryResultSuccess,
19
18
  GetConfigPathRequest,
20
19
  GetConfigPathResultSuccess,
20
+ GetConfigSchemaRequest,
21
+ GetConfigSchemaResultFailure,
22
+ GetConfigSchemaResultSuccess,
21
23
  GetConfigValueRequest,
22
24
  GetConfigValueResultFailure,
23
25
  GetConfigValueResultSuccess,
@@ -84,6 +86,9 @@ class ConfigManager:
84
86
  event_manager.assign_manager_to_request_type(GetConfigValueRequest, self.on_handle_get_config_value_request)
85
87
  event_manager.assign_manager_to_request_type(SetConfigValueRequest, self.on_handle_set_config_value_request)
86
88
  event_manager.assign_manager_to_request_type(GetConfigPathRequest, self.on_handle_get_config_path_request)
89
+ event_manager.assign_manager_to_request_type(
90
+ GetConfigSchemaRequest, self.on_handle_get_config_schema_request
91
+ )
87
92
  event_manager.assign_manager_to_request_type(ResetConfigRequest, self.on_handle_reset_config_request)
88
93
 
89
94
  event_manager.add_listener_to_app_event(
@@ -194,6 +199,9 @@ class ConfigManager:
194
199
  merged_config = merge_dicts(merged_config, self.env_config)
195
200
  logger.debug("Merged config from environment variables: %s", list(self.env_config.keys()))
196
201
 
202
+ # Re-assign workspace path in case env var overrides it
203
+ self.workspace_path = merged_config["workspace_directory"]
204
+
197
205
  # Validate the full config against the Settings model.
198
206
  try:
199
207
  Settings.model_validate(merged_config)
@@ -412,6 +420,39 @@ class ConfigManager:
412
420
  result_details = "Successfully returned the config path."
413
421
  return GetConfigPathResultSuccess(config_path=str(USER_CONFIG_PATH), result_details=result_details)
414
422
 
423
+ def on_handle_get_config_schema_request(self, request: GetConfigSchemaRequest) -> ResultPayload: # noqa: ARG002
424
+ """Handle request to get the configuration schema with current values and library settings.
425
+
426
+ This method returns a clean structure with three main components:
427
+ 1. base_schema: Core settings schema from Pydantic Settings model with categories
428
+ 2. library_schemas: Library-specific schemas from definition files (preserves enums)
429
+ 3. current_values: All current configuration values from merged config
430
+
431
+ The approach separates concerns for frontend flexibility and simplicity.
432
+ Library settings with explicit schemas (including enums) are preserved, while
433
+ libraries without schemas get simple object types.
434
+ """
435
+ try:
436
+ # Get base settings schema and current values
437
+ base_schema = Settings.model_json_schema()
438
+ current_values = self.merged_config.copy()
439
+
440
+ # Get library schemas
441
+ library_schemas = LibraryRegistry.get_all_library_schemas()
442
+
443
+ # Return clean structure
444
+ schema_with_defaults = {
445
+ "base_schema": base_schema,
446
+ "library_schemas": library_schemas,
447
+ "current_values": current_values,
448
+ }
449
+
450
+ result_details = "Successfully returned the configuration schema with default values and library settings."
451
+ return GetConfigSchemaResultSuccess(schema=schema_with_defaults, result_details=result_details)
452
+ except Exception as e:
453
+ result_details = f"Failed to generate configuration schema: {e}"
454
+ return GetConfigSchemaResultFailure(result_details=result_details)
455
+
415
456
  def on_handle_reset_config_request(self, request: ResetConfigRequest) -> ResultPayload: # noqa: ARG002
416
457
  try:
417
458
  self.reset_user_config()