griptape-nodes 0.34.2__py3-none-any.whl → 0.35.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 (31) hide show
  1. griptape_nodes/__init__.py +56 -41
  2. griptape_nodes/app/app.py +3 -3
  3. griptape_nodes/bootstrap/bootstrap_script.py +5 -3
  4. griptape_nodes/drivers/__init__.py +1 -0
  5. griptape_nodes/drivers/storage/__init__.py +4 -0
  6. griptape_nodes/drivers/storage/base_storage_driver.py +38 -0
  7. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +85 -0
  8. griptape_nodes/drivers/storage/local_storage_driver.py +49 -0
  9. griptape_nodes/exe_types/core_types.py +57 -1
  10. griptape_nodes/exe_types/flow.py +3 -1
  11. griptape_nodes/exe_types/node_types.py +34 -0
  12. griptape_nodes/machines/node_resolution.py +9 -1
  13. griptape_nodes/node_library/library_registry.py +2 -0
  14. griptape_nodes/retained_mode/events/execution_events.py +2 -0
  15. griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +1 -1
  16. griptape_nodes/retained_mode/events/node_events.py +13 -16
  17. griptape_nodes/retained_mode/events/parameter_events.py +10 -3
  18. griptape_nodes/retained_mode/events/static_file_events.py +26 -0
  19. griptape_nodes/retained_mode/griptape_nodes.py +3 -1
  20. griptape_nodes/retained_mode/managers/context_manager.py +123 -87
  21. griptape_nodes/retained_mode/managers/flow_manager.py +126 -86
  22. griptape_nodes/retained_mode/managers/library_manager.py +279 -95
  23. griptape_nodes/retained_mode/managers/node_manager.py +230 -159
  24. griptape_nodes/retained_mode/managers/settings.py +6 -1
  25. griptape_nodes/retained_mode/managers/static_files_manager.py +77 -54
  26. griptape_nodes/retained_mode/managers/workflow_manager.py +51 -12
  27. {griptape_nodes-0.34.2.dist-info → griptape_nodes-0.35.0.dist-info}/METADATA +1 -1
  28. {griptape_nodes-0.34.2.dist-info → griptape_nodes-0.35.0.dist-info}/RECORD +31 -26
  29. {griptape_nodes-0.34.2.dist-info → griptape_nodes-0.35.0.dist-info}/WHEEL +0 -0
  30. {griptape_nodes-0.34.2.dist-info → griptape_nodes-0.35.0.dist-info}/entry_points.txt +0 -0
  31. {griptape_nodes-0.34.2.dist-info → griptape_nodes-0.35.0.dist-info}/licenses/LICENSE +0 -0
@@ -9,6 +9,7 @@ with console.status("Loading Griptape Nodes...") as status:
9
9
  import argparse
10
10
  import importlib.metadata
11
11
  import json
12
+ import os
12
13
  import shutil
13
14
  import sys
14
15
  import tarfile
@@ -61,27 +62,37 @@ def main() -> None:
61
62
 
62
63
 
63
64
  def _run_init(
64
- *, api_key: str | None = None, workspace_directory: str | None = None, register_advanced_library: bool | None = None
65
+ *, workspace_directory: str | None = None, api_key: str | None = None, register_advanced_library: bool | None = None
65
66
  ) -> None:
66
- """Runs through the engine init steps, optionally skipping prompts if the user provided `--api-key`."""
67
+ """Runs through the engine init steps.
68
+
69
+ Args:
70
+ workspace_directory (str | None): The workspace directory to set.
71
+ api_key (str | None): The API key to set.
72
+ register_advanced_library (bool | None): Whether to register the advanced library.
73
+ """
67
74
  __init_system_config()
68
- _prompt_for_workspace(workspace_directory_arg=workspace_directory)
75
+ _prompt_for_workspace(workspace_directory=workspace_directory)
69
76
  _prompt_for_api_key(api_key=api_key)
70
77
  _prompt_for_libraries_to_register(register_advanced_library=register_advanced_library)
71
78
  _sync_assets()
72
79
  console.print("[bold green]Initialization complete![/bold green]")
73
80
 
74
81
 
75
- def _start_engine(*, no_update: bool) -> None:
82
+ def _start_engine(*, no_update: bool = False) -> None:
76
83
  """Starts the Griptape Nodes engine.
77
84
 
78
85
  Args:
79
- no_update: If True, skips the auto-update check.
86
+ no_update (bool): If True, skips the auto-update check.
80
87
  """
81
88
  if not CONFIG_DIR.exists():
82
89
  # Default init flow if there is no config directory
83
90
  console.print("[bold green]Config directory not found. Initializing...[/bold green]")
84
- _run_init()
91
+ _run_init(
92
+ workspace_directory=os.getenv("GTN_WORKSPACE_DIRECTORY"),
93
+ api_key=os.getenv("GTN_API_KEY"),
94
+ register_advanced_library=os.getenv("GTN_REGISTER_ADVANCED_LIBRARY", "false").lower() == "true",
95
+ )
85
96
 
86
97
  # Confusing double negation -- If `no_update` is set, we want to skip the update
87
98
  if not no_update:
@@ -115,15 +126,17 @@ def _get_args() -> argparse.Namespace:
115
126
  init_parser.add_argument(
116
127
  "--api-key",
117
128
  help="Set the Griptape Nodes API key.",
129
+ default=os.getenv("GTN_API_KEY", None),
118
130
  )
119
131
  init_parser.add_argument(
120
132
  "--workspace-directory",
121
133
  help="Set the Griptape Nodes workspace directory.",
134
+ default=os.getenv("GTN_WORKSPACE_DIRECTORY", None),
122
135
  )
136
+ register_advanced_library = os.getenv("GTN_REGISTER_ADVANCED_LIBRARY", None)
123
137
  init_parser.add_argument(
124
138
  "--register-advanced-library",
125
- action="store_true",
126
- default=None,
139
+ default=register_advanced_library.lower() == "true" if register_advanced_library is not None else None,
127
140
  help="Install the Griptape Nodes Advanced Image Library.",
128
141
  )
129
142
 
@@ -181,47 +194,46 @@ def _prompt_for_api_key(api_key: str | None = None) -> None:
181
194
  Once the key is generated, copy and paste its value here to proceed."""
182
195
  console.print(Panel(explainer, expand=False))
183
196
 
184
- default_key = api_key or secrets_manager.get_secret("GT_CLOUD_API_KEY", should_error_on_not_found=False)
185
- # If api_key is provided via --api-key, we don't want to prompt for it
186
- current_key = api_key
187
- while current_key is None:
188
- current_key = Prompt.ask(
189
- "Griptape API Key",
190
- default=default_key,
191
- show_default=True,
192
- )
197
+ default_api_key = secrets_manager.get_secret("GT_CLOUD_API_KEY", should_error_on_not_found=False)
198
+ while api_key is None:
199
+ api_key = Prompt.ask(
200
+ "Griptape API Key",
201
+ default=default_api_key,
202
+ show_default=True,
203
+ )
193
204
 
194
- secrets_manager.set_secret("GT_CLOUD_API_KEY", current_key)
205
+ secrets_manager.set_secret("GT_CLOUD_API_KEY", api_key)
195
206
  console.print("[bold green]Griptape API Key set")
196
207
 
197
208
 
198
- def _prompt_for_workspace(*, workspace_directory_arg: str | None) -> None:
209
+ def _prompt_for_workspace(*, workspace_directory: str | None) -> None:
199
210
  """Prompts the user for their workspace directory and stores it in config directory."""
200
- if workspace_directory_arg is None:
211
+ if workspace_directory is None:
201
212
  explainer = """[bold cyan]Workspace Directory[/bold cyan]
202
213
  Select the workspace directory. This is the location where Griptape Nodes will store your saved workflows.
203
214
  You may enter a custom directory or press Return to accept the default workspace directory"""
204
215
  console.print(Panel(explainer, expand=False))
205
216
 
206
- valid_workspace = False
207
- default_workspace_directory = workspace_directory_arg or config_manager.get_config_value("workspace_directory")
208
- while not (valid_workspace or workspace_directory_arg):
209
- try:
210
- workspace_directory = Prompt.ask(
211
- "Workspace Directory",
212
- default=default_workspace_directory,
213
- show_default=True,
214
- )
215
- workspace_path = Path(workspace_directory).expanduser().resolve()
216
-
217
- config_manager.workspace_path = workspace_path
218
- config_manager.set_config_value("workspace_directory", str(workspace_path))
217
+ default_workspace_directory = workspace_directory or config_manager.get_config_value("workspace_directory")
218
+ while workspace_directory is None:
219
+ try:
220
+ workspace_to_test = Prompt.ask(
221
+ "Workspace Directory",
222
+ default=default_workspace_directory,
223
+ show_default=True,
224
+ )
225
+ # Try to resolve the path to check if it exists
226
+ if workspace_to_test is not None:
227
+ Path(workspace_to_test).expanduser().resolve()
228
+ workspace_directory = workspace_to_test
229
+ except OSError as e:
230
+ console.print(f"[bold red]Invalid workspace directory: {e}[/bold red]")
231
+ except json.JSONDecodeError as e:
232
+ console.print(f"[bold red]Error reading config file: {e}[/bold red]")
233
+
234
+ workspace_path = Path(workspace_directory).expanduser().resolve()
235
+ config_manager.set_config_value("workspace_directory", str(workspace_path))
219
236
 
220
- valid_workspace = True
221
- except OSError as e:
222
- console.print(f"[bold red]Invalid workspace directory: {e}[/bold red]")
223
- except json.JSONDecodeError as e:
224
- console.print(f"[bold red]Error reading config file: {e}[/bold red]")
225
237
  console.print(f"[bold green]Workspace directory set to: {config_manager.workspace_path}[/bold green]")
226
238
 
227
239
 
@@ -429,6 +441,7 @@ def _uninstall_self() -> None:
429
441
  # Remove config and data directories
430
442
  console.print("[bold]Removing config and data directories...[/bold]")
431
443
  dirs = [(CONFIG_DIR, "Config Dir"), (DATA_DIR, "Data Dir")]
444
+ caveats = []
432
445
  for dir_path, dir_name in dirs:
433
446
  if dir_path.exists():
434
447
  console.print(f"[bold]Removing {dir_name} '{dir_path}'...[/bold]")
@@ -436,10 +449,12 @@ def _uninstall_self() -> None:
436
449
  shutil.rmtree(dir_path)
437
450
  except OSError as exc:
438
451
  console.print(f"[red]Error removing {dir_name} '{dir_path}': {exc}[/red]")
452
+ caveats.append(
453
+ f"- [red]Error removing {dir_name} '{dir_path}'. You may want remove this directory manually.[/red]"
454
+ )
439
455
  else:
440
456
  console.print(f"[yellow]{dir_name} '{dir_path}' does not exist; skipping.[/yellow]")
441
457
 
442
- caveats = []
443
458
  # Handle any remaining config files not removed by design
444
459
  remaining_config_files = config_manager.config_files
445
460
  if remaining_config_files:
@@ -461,8 +476,8 @@ def _uninstall_self() -> None:
461
476
  def _process_args(args: argparse.Namespace) -> None: # noqa: C901, PLR0912
462
477
  if args.command == "init":
463
478
  _run_init(
464
- api_key=args.api_key,
465
479
  workspace_directory=args.workspace_directory,
480
+ api_key=args.api_key,
466
481
  register_advanced_library=args.register_advanced_library,
467
482
  )
468
483
  elif args.command == "engine":
@@ -529,7 +544,7 @@ def __init_system_config() -> None:
529
544
  for file_name in files_to_create:
530
545
  file_path = CONFIG_DIR / file_name[0]
531
546
  if not file_path.exists():
532
- with Path.open(file_path, "w") as file:
547
+ with Path.open(file_path, "w", encoding="utf-8") as file:
533
548
  file.write(file_name[1])
534
549
 
535
550
 
griptape_nodes/app/app.py CHANGED
@@ -16,7 +16,7 @@ from urllib.parse import urljoin
16
16
  import httpx
17
17
  import uvicorn
18
18
  from dotenv import get_key
19
- from fastapi import FastAPI, HTTPException, Request, UploadFile
19
+ from fastapi import FastAPI, HTTPException, Request
20
20
  from fastapi.middleware.cors import CORSMiddleware
21
21
  from fastapi.staticfiles import StaticFiles
22
22
  from griptape.events import (
@@ -154,15 +154,15 @@ def _serve_static_server() -> None:
154
154
  return {"url": url}
155
155
 
156
156
  @app.put("/static-uploads/{file_name:str}")
157
- async def create_static_file(file: UploadFile, file_name: str) -> dict:
157
+ async def create_static_file(request: Request, file_name: str) -> dict:
158
158
  """Upload a static file to the static server."""
159
159
  if not STATIC_SERVER_ENABLED:
160
160
  msg = "Static server is not enabled. Please set STATIC_SERVER_ENABLED to True."
161
161
  raise ValueError(msg)
162
162
 
163
- data = await file.read()
164
163
  if not static_dir.exists():
165
164
  static_dir.mkdir(parents=True, exist_ok=True)
165
+ data = await request.body()
166
166
  try:
167
167
  Path(static_dir / file_name).write_bytes(data)
168
168
  except binascii.Error as e:
@@ -17,6 +17,7 @@ from register_libraries_script import ( # type: ignore[import] - This import is
17
17
  register_libraries,
18
18
  )
19
19
 
20
+ from griptape_nodes.exe_types.flow import ControlFlow
20
21
  from griptape_nodes.exe_types.node_types import EndNode, StartNode
21
22
  from griptape_nodes.retained_mode.events.base_events import (
22
23
  AppEvent,
@@ -63,9 +64,9 @@ def _load_user_workflow(path_to_workflow: str) -> None:
63
64
  spec.loader.exec_module(module)
64
65
 
65
66
 
66
- def _load_flow_for_workflow() -> str:
67
+ def _load_flow_for_workflow() -> ControlFlow:
67
68
  context_manager = GriptapeNodes.ContextManager()
68
- return context_manager.get_current_flow_name()
69
+ return context_manager.get_current_flow()
69
70
 
70
71
 
71
72
  def _set_workflow_context(workflow_name: str) -> None:
@@ -208,7 +209,8 @@ def run(workflow_name: str, flow_input: Any) -> None:
208
209
  # or nothing works. The name can be anything, but how about the workflow_name.
209
210
  _set_workflow_context(workflow_name=workflow_name)
210
211
  _load_user_workflow("workflow.py")
211
- flow_name = _load_flow_for_workflow()
212
+ flow = _load_flow_for_workflow()
213
+ flow_name = flow.name
212
214
  # Now let's set the input to the flow
213
215
  _set_input_for_flow(flow_name=flow_name, flow_input=flow_input)
214
216
 
@@ -0,0 +1 @@
1
+ """Package for griptape nodes drivers."""
@@ -0,0 +1,4 @@
1
+ """Package for griptape nodes storage drivers.
2
+
3
+ Storage drivers are responsible for managing the storage and retrieval of files.
4
+ """
@@ -0,0 +1,38 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import TypedDict
3
+
4
+
5
+ class CreateSignedUploadUrlResponse(TypedDict):
6
+ """Response type for create_signed_upload_url method."""
7
+
8
+ url: str
9
+ headers: dict
10
+ method: str
11
+
12
+
13
+ class BaseStorageDriver(ABC):
14
+ """Base class for storage drivers."""
15
+
16
+ @abstractmethod
17
+ def create_signed_upload_url(self, file_name: str) -> CreateSignedUploadUrlResponse:
18
+ """Create a signed upload URL for the given file name.
19
+
20
+ Args:
21
+ file_name: The name of the file to create a signed URL for.
22
+
23
+ Returns:
24
+ CreateSignedUploadUrlResponse: A dictionary containing the signed URL, headers, and operation type.
25
+ """
26
+ ...
27
+
28
+ @abstractmethod
29
+ def create_signed_download_url(self, file_name: str) -> str:
30
+ """Create a signed download URL for the given file name.
31
+
32
+ Args:
33
+ file_name: The name of the file to create a signed URL for.
34
+
35
+ Returns:
36
+ str: The signed URL for downloading the file.
37
+ """
38
+ ...
@@ -0,0 +1,85 @@
1
+ import logging
2
+ import os
3
+ from urllib.parse import urljoin
4
+
5
+ import httpx
6
+
7
+ from griptape_nodes.drivers.storage.base_storage_driver import BaseStorageDriver, CreateSignedUploadUrlResponse
8
+
9
+ logger = logging.getLogger("griptape_nodes")
10
+
11
+
12
+ class GriptapeCloudStorageDriver(BaseStorageDriver):
13
+ """Stores files using the Griptape Cloud's Asset APIs."""
14
+
15
+ def __init__(
16
+ self,
17
+ *,
18
+ bucket_id: str | None = None,
19
+ base_url: str | None = None,
20
+ api_key: str | None = None,
21
+ headers: dict | None = None,
22
+ ) -> None:
23
+ """Initialize the GriptapeCloudStorageDriver.
24
+
25
+ Args:
26
+ bucket_id: The ID of the bucket to use. If not provided, a new bucket will be provisioned.
27
+ base_url: The base URL for the Griptape Cloud API. If not provided, it will be retrieved from the environment variable "GT_CLOUD_BASE_URL" or default to "https://cloud.griptape.ai".
28
+ api_key: The API key for authentication. If not provided, it will be retrieved from the environment variable "GT_CLOUD_API_KEY".
29
+ headers: Additional headers to include in the requests. If not provided, the default headers will be used.
30
+ """
31
+ self.base_url = (
32
+ base_url if base_url is not None else os.environ.get("GT_CLOUD_BASE_URL", "https://cloud.griptape.ai")
33
+ )
34
+ self.api_key = api_key if api_key is not None else os.environ.get("GT_CLOUD_API_KEY")
35
+ self.headers = (
36
+ headers
37
+ if headers is not None
38
+ else {
39
+ "Authorization": f"Bearer {self.api_key}",
40
+ }
41
+ )
42
+
43
+ self.bucket_id = bucket_id
44
+
45
+ def create_signed_upload_url(self, file_name: str) -> CreateSignedUploadUrlResponse:
46
+ self._create_asset(file_name)
47
+
48
+ url = urljoin(self.base_url, f"/api/buckets/{self.bucket_id}/asset-urls/{file_name}")
49
+ try:
50
+ response = httpx.post(url, json={"operation": "PUT"}, headers=self.headers)
51
+ response.raise_for_status()
52
+ except httpx.HTTPStatusError as e:
53
+ msg = f"Failed to create presigned URL for file {file_name}: {e}"
54
+ logger.error(msg)
55
+ raise RuntimeError(msg) from e
56
+
57
+ response_data = response.json()
58
+
59
+ return {"url": response_data["url"], "headers": response_data.get("headers", {}), "method": "PUT"}
60
+
61
+ def create_signed_download_url(self, file_name: str) -> str:
62
+ url = urljoin(self.base_url, f"/api/buckets/{self.bucket_id}/asset-urls/{file_name}")
63
+ try:
64
+ response = httpx.post(url, json={"method": "GET"}, headers=self.headers)
65
+ response.raise_for_status()
66
+ except httpx.HTTPStatusError as e:
67
+ msg = f"Failed to create presigned URL for file {file_name}: {e}"
68
+ logger.error(msg)
69
+ raise RuntimeError(msg) from e
70
+
71
+ response_data = response.json()
72
+
73
+ return response_data["url"]
74
+
75
+ def _create_asset(self, asset_name: str) -> str:
76
+ url = urljoin(self.base_url, f"/api/buckets/{self.bucket_id}/assets")
77
+ try:
78
+ response = httpx.put(url=url, json={"name": asset_name}, headers=self.headers)
79
+ response.raise_for_status()
80
+ except httpx.HTTPStatusError as e:
81
+ msg = str(e)
82
+ logger.error(msg)
83
+ raise ValueError(msg) from e
84
+
85
+ return response.json()["name"]
@@ -0,0 +1,49 @@
1
+ import logging
2
+ from urllib.parse import urljoin
3
+
4
+ import httpx
5
+
6
+ from griptape_nodes.app.app import STATIC_SERVER_ENABLED, STATIC_SERVER_HOST, STATIC_SERVER_PORT, STATIC_SERVER_URL
7
+ from griptape_nodes.drivers.storage.base_storage_driver import BaseStorageDriver, CreateSignedUploadUrlResponse
8
+
9
+ logger = logging.getLogger("griptape_nodes")
10
+
11
+
12
+ class LocalStorageDriver(BaseStorageDriver):
13
+ """Stores files using the engine's local static server."""
14
+
15
+ def __init__(self, base_url: str | None = None) -> None:
16
+ """Initialize the LocalStorageDriver.
17
+
18
+ Args:
19
+ base_url: The base URL for the static file server. If not provided, it will be constructed
20
+ """
21
+ if not STATIC_SERVER_ENABLED:
22
+ msg = "Static server is not enabled. Please set STATIC_SERVER_ENABLED to True."
23
+ raise ValueError(msg)
24
+ if base_url is None:
25
+ self.base_url = f"http://{STATIC_SERVER_HOST}:{STATIC_SERVER_PORT}{STATIC_SERVER_URL}"
26
+ else:
27
+ self.base_url = base_url
28
+
29
+ def create_signed_upload_url(self, file_name: str) -> CreateSignedUploadUrlResponse:
30
+ static_url = urljoin(self.base_url, "/static-upload-urls")
31
+ try:
32
+ response = httpx.post(static_url, json={"file_name": file_name})
33
+ response.raise_for_status()
34
+ except httpx.HTTPStatusError as e:
35
+ msg = f"Failed to create presigned URL for file {file_name}: {e}"
36
+ logger.error(msg)
37
+ raise RuntimeError(msg) from e
38
+
39
+ response_data = response.json()
40
+ url = response_data.get("url")
41
+ if url is None:
42
+ msg = f"Failed to create presigned URL for file {file_name}: {response_data}"
43
+ logger.error(msg)
44
+ raise ValueError(msg)
45
+
46
+ return {"url": url, "headers": response_data.get("headers", {}), "method": "PUT"}
47
+
48
+ def create_signed_download_url(self, file_name: str) -> str:
49
+ return urljoin(self.base_url, f"/static/{file_name}")
@@ -5,7 +5,7 @@ from abc import ABC, abstractmethod
5
5
  from copy import deepcopy
6
6
  from dataclasses import dataclass, field
7
7
  from enum import Enum, auto
8
- from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, Self, TypeVar
8
+ from typing import TYPE_CHECKING, Any, ClassVar, Literal, NamedTuple, Self, TypeVar
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from collections.abc import Callable
@@ -272,6 +272,62 @@ class BaseNodeElement:
272
272
  return cls._stack[-1] if cls._stack else None
273
273
 
274
274
 
275
+ @dataclass(kw_only=True)
276
+ class ParameterMessage(BaseNodeElement):
277
+ """Represents a UI message element, such as a warning or informational text."""
278
+
279
+ # Define default titles as a class-level constant
280
+ DEFAULT_TITLES: ClassVar[dict[str, str]] = {
281
+ "info": "Info",
282
+ "warning": "Warning",
283
+ "error": "Error",
284
+ "success": "Success",
285
+ "tip": "Tip",
286
+ "none": "",
287
+ }
288
+
289
+ # Create a type alias using the keys from DEFAULT_TITLES
290
+ type VariantType = Literal["info", "warning", "error", "success", "tip", "none"]
291
+
292
+ element_type: str = field(default_factory=lambda: ParameterMessage.__name__)
293
+ variant: VariantType
294
+ title: str | None = None
295
+ value: str
296
+ button_link: str | None = None
297
+ button_text: str | None = None
298
+ full_width: bool = False
299
+ ui_options: dict = field(default_factory=dict)
300
+
301
+ def to_dict(self) -> dict[str, Any]:
302
+ data = super().to_dict()
303
+
304
+ # Use class-level default titles
305
+ title = self.title or self.DEFAULT_TITLES.get(str(self.variant), "")
306
+
307
+ # Merge the UI options with the message-specific options
308
+ merged_ui_options = {
309
+ **self.ui_options,
310
+ **{
311
+ k: v
312
+ for k, v in {
313
+ "title": title,
314
+ "variant": self.variant,
315
+ "button_link": self.button_link,
316
+ "button_text": self.button_text,
317
+ "full_width": self.full_width,
318
+ }.items()
319
+ if v is not None
320
+ },
321
+ }
322
+
323
+ data["name"] = self.name
324
+ data["value"] = self.value
325
+ data["default_value"] = self.value # for compatibility
326
+ data["ui_options"] = merged_ui_options
327
+
328
+ return data
329
+
330
+
275
331
  @dataclass(kw_only=True)
276
332
  class ParameterGroup(BaseNodeElement):
277
333
  """UI element for a group of parameters."""
@@ -30,13 +30,15 @@ class CurrentNodes(NamedTuple):
30
30
 
31
31
  # The flow will own all of the nodes and the connections
32
32
  class ControlFlow:
33
+ name: str
33
34
  connections: Connections
34
35
  nodes: dict[str, BaseNode]
35
36
  control_flow_machine: ControlFlowMachine
36
37
  single_node_resolution: bool
37
38
  flow_queue: Queue[BaseNode]
38
39
 
39
- def __init__(self) -> None:
40
+ def __init__(self, name: str) -> None:
41
+ self.name = name
40
42
  self.connections = Connections()
41
43
  self.nodes = {}
42
44
  self.control_flow_machine = ControlFlowMachine(self)
@@ -17,6 +17,7 @@ from griptape_nodes.exe_types.core_types import (
17
17
  ParameterDictionary,
18
18
  ParameterGroup,
19
19
  ParameterList,
20
+ ParameterMessage,
20
21
  ParameterMode,
21
22
  ParameterTypeBuiltin,
22
23
  )
@@ -204,6 +205,11 @@ class BaseNode(ABC):
204
205
  # Default behavior is to do nothing, and indicate no other modified Parameters.
205
206
  return None # noqa: RET501
206
207
 
208
+ def after_settings_changed(self, modified_parameters_set: set[str]) -> None: # noqa: ARG002
209
+ """Callback for when the settings of this Node are changed."""
210
+ # Waiting for https://github.com/griptape-ai/griptape-nodes/issues/1309
211
+ return
212
+
207
213
  def on_griptape_event(self, event: BaseEvent) -> None: # noqa: ARG002
208
214
  """Callback for when a Griptape Event comes destined for this Node."""
209
215
  return
@@ -265,6 +271,34 @@ class BaseNode(ABC):
265
271
  if parameter is not None:
266
272
  parameter._ui_options["hide"] = not visible
267
273
 
274
+ def get_message_by_name_or_element_id(self, element: str) -> ParameterMessage | None:
275
+ element_items = self.root_ui_element.find_elements_by_type(ParameterMessage)
276
+ for element_item in element_items:
277
+ if element in (element_item.name, element_item.element_id):
278
+ return element_item
279
+ return None
280
+
281
+ def _set_message_visibility(self, names: str | list[str], *, visible: bool) -> None:
282
+ """Sets the visibility of one or more messages.
283
+
284
+ Args:
285
+ names (str or list of str): The message name(s) to update.
286
+ visible (bool): Whether to show (True) or hide (False) the messages.
287
+ """
288
+ if isinstance(names, str):
289
+ names = [names]
290
+
291
+ for name in names:
292
+ message = self.get_message_by_name_or_element_id(name)
293
+ if message is not None:
294
+ message.ui_options["hide"] = not visible
295
+
296
+ def hide_message_by_name(self, names: str | list[str]) -> None:
297
+ self._set_message_visibility(names, visible=False)
298
+
299
+ def show_message_by_name(self, names: str | list[str]) -> None:
300
+ self._set_message_visibility(names, visible=True)
301
+
268
302
  def hide_parameter_by_name(self, names: str | list[str]) -> None:
269
303
  """Hides one or more parameters by name."""
270
304
  self._set_parameter_visibility(names, visible=False)
@@ -13,6 +13,7 @@ from griptape_nodes.exe_types.core_types import ParameterTypeBuiltin
13
13
  from griptape_nodes.exe_types.node_types import BaseNode, NodeResolutionState
14
14
  from griptape_nodes.exe_types.type_validator import TypeValidator
15
15
  from griptape_nodes.machines.fsm import FSM, State
16
+ from griptape_nodes.node_library.library_registry import LibraryRegistry
16
17
  from griptape_nodes.retained_mode.events.base_events import (
17
18
  ExecutionEvent,
18
19
  ExecutionGriptapeNodeEvent,
@@ -216,7 +217,7 @@ class ExecuteNodeState(State):
216
217
  modified_parameter = current_node.get_parameter_by_name(modified_parameter_name)
217
218
  if modified_parameter is not None:
218
219
  modified_request = AlterParameterEvent.create(
219
- node_name=current_node.name, parameter=modified_parameter
220
+ node=current_node, parameter=modified_parameter
220
221
  )
221
222
  EventBus.publish_event(
222
223
  ExecutionGriptapeNodeEvent(ExecutionEvent(payload=modified_request))
@@ -339,12 +340,19 @@ class ExecuteNodeState(State):
339
340
  )
340
341
 
341
342
  # Output values should already be saved!
343
+ library = LibraryRegistry.get_libraries_with_node_type(current_node.__class__.__name__)
344
+ if len(library) == 1:
345
+ library_name = library[0]
346
+ else:
347
+ library_name = None
342
348
  EventBus.publish_event(
343
349
  ExecutionGriptapeNodeEvent(
344
350
  wrapped_event=ExecutionEvent(
345
351
  payload=NodeResolvedEvent(
346
352
  node_name=current_node.name,
347
353
  parameter_output_values=TypeValidator.safe_serialize(current_node.parameter_output_values),
354
+ node_type=current_node.__class__.__name__,
355
+ specific_library_name=library_name,
348
356
  )
349
357
  )
350
358
  )
@@ -84,6 +84,8 @@ class LibrarySchema(BaseModel):
84
84
  library itself.
85
85
  """
86
86
 
87
+ LATEST_SCHEMA_VERSION: ClassVar[str] = "0.1.0"
88
+
87
89
  name: str
88
90
  library_schema_version: str
89
91
  metadata: LibraryMetadata
@@ -230,6 +230,8 @@ class ControlFlowCancelledEvent(ExecutionPayload):
230
230
  class NodeResolvedEvent(ExecutionPayload):
231
231
  node_name: str
232
232
  parameter_output_values: dict
233
+ node_type: str
234
+ specific_library_name: str | None = None
233
235
 
234
236
 
235
237
  @dataclass
@@ -26,5 +26,5 @@ for payload_class_name, payload_class in payload_dict.items():
26
26
  else:
27
27
  logger.info("Skipping %s as it is not a RequestPayload.", payload_class_name)
28
28
 
29
- with Path("request_payload_schemas.json").open("w+") as file:
29
+ with Path("request_payload_schemas.json").open("w+", encoding="utf-8") as file:
30
30
  file.write(json.dumps(list(payload_type_to_schema.values()), indent=2))