griptape-nodes 0.52.0__py3-none-any.whl → 0.53.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/__init__.py +6 -943
- griptape_nodes/__main__.py +6 -0
- griptape_nodes/app/api.py +1 -12
- griptape_nodes/app/app.py +256 -209
- griptape_nodes/cli/__init__.py +1 -0
- griptape_nodes/cli/commands/__init__.py +1 -0
- griptape_nodes/cli/commands/config.py +71 -0
- griptape_nodes/cli/commands/engine.py +80 -0
- griptape_nodes/cli/commands/init.py +548 -0
- griptape_nodes/cli/commands/libraries.py +90 -0
- griptape_nodes/cli/commands/self.py +117 -0
- griptape_nodes/cli/main.py +46 -0
- griptape_nodes/cli/shared.py +84 -0
- griptape_nodes/common/__init__.py +1 -0
- griptape_nodes/common/directed_graph.py +55 -0
- griptape_nodes/drivers/storage/local_storage_driver.py +7 -2
- griptape_nodes/exe_types/core_types.py +60 -2
- griptape_nodes/exe_types/node_types.py +38 -24
- griptape_nodes/machines/control_flow.py +86 -22
- griptape_nodes/machines/fsm.py +10 -1
- griptape_nodes/machines/parallel_resolution.py +570 -0
- griptape_nodes/machines/{node_resolution.py → sequential_resolution.py} +22 -51
- griptape_nodes/mcp_server/server.py +1 -1
- griptape_nodes/retained_mode/events/base_events.py +2 -2
- griptape_nodes/retained_mode/events/node_events.py +4 -3
- griptape_nodes/retained_mode/griptape_nodes.py +25 -12
- griptape_nodes/retained_mode/managers/agent_manager.py +9 -5
- griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +3 -1
- griptape_nodes/retained_mode/managers/context_manager.py +6 -5
- griptape_nodes/retained_mode/managers/flow_manager.py +117 -204
- griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +1 -1
- griptape_nodes/retained_mode/managers/library_manager.py +35 -25
- griptape_nodes/retained_mode/managers/node_manager.py +81 -199
- griptape_nodes/retained_mode/managers/object_manager.py +11 -5
- griptape_nodes/retained_mode/managers/os_manager.py +24 -9
- griptape_nodes/retained_mode/managers/secrets_manager.py +8 -4
- griptape_nodes/retained_mode/managers/settings.py +32 -1
- griptape_nodes/retained_mode/managers/static_files_manager.py +8 -3
- griptape_nodes/retained_mode/managers/sync_manager.py +8 -5
- griptape_nodes/retained_mode/managers/workflow_manager.py +110 -122
- griptape_nodes/traits/add_param_button.py +1 -1
- griptape_nodes/traits/button.py +216 -6
- griptape_nodes/traits/color_picker.py +66 -0
- griptape_nodes/traits/traits.json +4 -0
- {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.53.0.dist-info}/METADATA +2 -1
- {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.53.0.dist-info}/RECORD +48 -34
- {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.53.0.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.52.0.dist-info → griptape_nodes-0.53.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Libraries command for Griptape Nodes CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import shutil
|
|
5
|
+
import tarfile
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
import typer
|
|
11
|
+
from rich.progress import Progress
|
|
12
|
+
|
|
13
|
+
from griptape_nodes.cli.shared import (
|
|
14
|
+
ENV_LIBRARIES_BASE_DIR,
|
|
15
|
+
LATEST_TAG,
|
|
16
|
+
NODES_TARBALL_URL,
|
|
17
|
+
console,
|
|
18
|
+
)
|
|
19
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
20
|
+
from griptape_nodes.utils.version_utils import get_current_version, get_install_source
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(help="Manage local libraries.")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command()
|
|
26
|
+
def sync() -> None:
|
|
27
|
+
"""Sync libraries with your current engine version."""
|
|
28
|
+
asyncio.run(_sync_libraries())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def _sync_libraries() -> None:
|
|
32
|
+
"""Download and sync Griptape Nodes libraries, copying only directories from synced libraries."""
|
|
33
|
+
install_source, _ = get_install_source()
|
|
34
|
+
# Unless we're installed from PyPi, grab libraries from the 'latest' tag
|
|
35
|
+
if install_source == "pypi":
|
|
36
|
+
version = get_current_version()
|
|
37
|
+
else:
|
|
38
|
+
version = LATEST_TAG
|
|
39
|
+
|
|
40
|
+
console.print(f"[bold cyan]Fetching Griptape Nodes libraries ({version})...[/bold cyan]")
|
|
41
|
+
|
|
42
|
+
tar_url = NODES_TARBALL_URL.format(tag=version)
|
|
43
|
+
console.print(f"[green]Downloading from {tar_url}[/green]")
|
|
44
|
+
dest_nodes = Path(ENV_LIBRARIES_BASE_DIR)
|
|
45
|
+
|
|
46
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
47
|
+
tar_path = Path(tmp) / "nodes.tar.gz"
|
|
48
|
+
|
|
49
|
+
# Streaming download with a tiny progress bar
|
|
50
|
+
with httpx.stream("GET", tar_url, follow_redirects=True) as r, Progress() as progress:
|
|
51
|
+
task = progress.add_task("[green]Downloading...", total=int(r.headers.get("Content-Length", 0)))
|
|
52
|
+
progress.start()
|
|
53
|
+
try:
|
|
54
|
+
r.raise_for_status()
|
|
55
|
+
except httpx.HTTPStatusError as e:
|
|
56
|
+
console.print(f"[red]Error fetching libraries: {e}[/red]")
|
|
57
|
+
return
|
|
58
|
+
with tar_path.open("wb") as f:
|
|
59
|
+
for chunk in r.iter_bytes():
|
|
60
|
+
f.write(chunk)
|
|
61
|
+
progress.update(task, advance=len(chunk))
|
|
62
|
+
|
|
63
|
+
console.print("[green]Extracting...[/green]")
|
|
64
|
+
# Extract and locate extracted directory
|
|
65
|
+
with tarfile.open(tar_path) as tar:
|
|
66
|
+
tar.extractall(tmp, filter="data")
|
|
67
|
+
|
|
68
|
+
extracted_root = next(Path(tmp).glob("griptape-nodes-*"))
|
|
69
|
+
extracted_libs = extracted_root / "libraries"
|
|
70
|
+
|
|
71
|
+
# Copy directories from synced libraries without removing existing content
|
|
72
|
+
console.print(f"[green]Syncing libraries to {dest_nodes.resolve()}...[/green]")
|
|
73
|
+
dest_nodes.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
for library_dir in extracted_libs.iterdir():
|
|
75
|
+
if library_dir.is_dir():
|
|
76
|
+
dest_library_dir = dest_nodes / library_dir.name
|
|
77
|
+
if dest_library_dir.exists():
|
|
78
|
+
shutil.rmtree(dest_library_dir)
|
|
79
|
+
shutil.copytree(library_dir, dest_library_dir)
|
|
80
|
+
console.print(f"[green]Synced library: {library_dir.name}[/green]")
|
|
81
|
+
|
|
82
|
+
# Re-initialize all libraries from config
|
|
83
|
+
console.print("[bold cyan]Initializing libraries...[/bold cyan]")
|
|
84
|
+
try:
|
|
85
|
+
await GriptapeNodes.LibraryManager().load_all_libraries_from_config()
|
|
86
|
+
console.print("[bold green]Libraries Initialized successfully.[/bold green]")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
console.print(f"[red]Error initializing libraries: {e}[/red]")
|
|
89
|
+
|
|
90
|
+
console.print("[bold green]Libraries synced.[/bold green]")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Self command for Griptape Nodes CLI."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from griptape_nodes.cli.shared import (
|
|
9
|
+
CONFIG_DIR,
|
|
10
|
+
DATA_DIR,
|
|
11
|
+
GITHUB_UPDATE_URL,
|
|
12
|
+
LATEST_TAG,
|
|
13
|
+
PYPI_UPDATE_URL,
|
|
14
|
+
config_manager,
|
|
15
|
+
console,
|
|
16
|
+
os_manager,
|
|
17
|
+
)
|
|
18
|
+
from griptape_nodes.utils.uv_utils import find_uv_bin
|
|
19
|
+
from griptape_nodes.utils.version_utils import (
|
|
20
|
+
get_complete_version_string,
|
|
21
|
+
get_current_version,
|
|
22
|
+
get_latest_version_git,
|
|
23
|
+
get_latest_version_pypi,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(help="Manage this CLI installation.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
def update() -> None:
|
|
31
|
+
"""Update the CLI."""
|
|
32
|
+
_update_self()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command()
|
|
36
|
+
def uninstall() -> None:
|
|
37
|
+
"""Uninstall the CLI."""
|
|
38
|
+
_uninstall_self()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command()
|
|
42
|
+
def version() -> None:
|
|
43
|
+
"""Print the CLI version."""
|
|
44
|
+
_print_current_version()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get_latest_version(package: str, install_source: str) -> str:
|
|
48
|
+
"""Fetches the latest release tag from PyPI.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
package: The name of the package to fetch the latest version for.
|
|
52
|
+
install_source: The source from which the package is installed (e.g., "pypi", "git", "file").
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
str: Latest release tag (e.g., "v0.31.4")
|
|
56
|
+
"""
|
|
57
|
+
if install_source == "pypi":
|
|
58
|
+
return get_latest_version_pypi(package, PYPI_UPDATE_URL)
|
|
59
|
+
if install_source == "git":
|
|
60
|
+
return get_latest_version_git(package, GITHUB_UPDATE_URL, LATEST_TAG)
|
|
61
|
+
# If the package is installed from a file, just return the current version since the user is likely managing it manually
|
|
62
|
+
return get_current_version()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _update_self() -> None:
|
|
66
|
+
"""Installs the latest release of the CLI *and* refreshes bundled libraries."""
|
|
67
|
+
console.print("[bold green]Starting updater...[/bold green]")
|
|
68
|
+
|
|
69
|
+
os_manager.replace_process([sys.executable, "-m", "griptape_nodes.updater"])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _print_current_version() -> None:
|
|
73
|
+
"""Prints the current version of the script."""
|
|
74
|
+
version_string = get_complete_version_string()
|
|
75
|
+
console.print(f"[bold green]{version_string}[/bold green]")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _uninstall_self() -> None:
|
|
79
|
+
"""Uninstalls itself by removing config/data directories and the executable."""
|
|
80
|
+
console.print("[bold]Uninstalling Griptape Nodes...[/bold]")
|
|
81
|
+
|
|
82
|
+
# Remove config and data directories
|
|
83
|
+
console.print("[bold]Removing config and data directories...[/bold]")
|
|
84
|
+
dirs = [(CONFIG_DIR, "Config Dir"), (DATA_DIR, "Data Dir")]
|
|
85
|
+
caveats = []
|
|
86
|
+
for dir_path, dir_name in dirs:
|
|
87
|
+
if dir_path.exists():
|
|
88
|
+
console.print(f"[bold]Removing {dir_name} '{dir_path}'...[/bold]")
|
|
89
|
+
try:
|
|
90
|
+
shutil.rmtree(dir_path)
|
|
91
|
+
except OSError as exc:
|
|
92
|
+
console.print(f"[red]Error removing {dir_name} '{dir_path}': {exc}[/red]")
|
|
93
|
+
caveats.append(
|
|
94
|
+
f"- [red]Error removing {dir_name} '{dir_path}'. You may want remove this directory manually.[/red]"
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
console.print(f"[yellow]{dir_name} '{dir_path}' does not exist; skipping.[/yellow]")
|
|
98
|
+
|
|
99
|
+
# Handle any remaining config files not removed by design
|
|
100
|
+
remaining_config_files = config_manager.config_files
|
|
101
|
+
if remaining_config_files:
|
|
102
|
+
caveats.append("- Some config files were intentionally not removed:")
|
|
103
|
+
caveats.extend(f"\t[yellow]- {file}[/yellow]" for file in remaining_config_files)
|
|
104
|
+
|
|
105
|
+
# If there were any caveats to the uninstallation process, print them
|
|
106
|
+
if caveats:
|
|
107
|
+
console.print("[bold]Caveats:[/bold]")
|
|
108
|
+
for line in caveats:
|
|
109
|
+
console.print(line)
|
|
110
|
+
|
|
111
|
+
# Remove the executable
|
|
112
|
+
console.print("[bold]Removing the executable...[/bold]")
|
|
113
|
+
console.print("[bold yellow]When done, press Enter to exit.[/bold yellow]")
|
|
114
|
+
|
|
115
|
+
# Remove the tool using UV
|
|
116
|
+
uv_path = find_uv_bin()
|
|
117
|
+
os_manager.replace_process([uv_path, "tool", "uninstall", "griptape-nodes"])
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Main CLI application using typer."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
# Add current directory to path for imports to work
|
|
10
|
+
sys.path.append(str(Path.cwd()))
|
|
11
|
+
|
|
12
|
+
from griptape_nodes.cli.commands import config, engine, init, libraries, self
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(
|
|
17
|
+
name="griptape-nodes",
|
|
18
|
+
help="Griptape Nodes Engine CLI",
|
|
19
|
+
no_args_is_help=False,
|
|
20
|
+
rich_markup_mode="rich",
|
|
21
|
+
invoke_without_command=True,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Add subcommands
|
|
25
|
+
app.command("init", help="Initialize engine configuration.")(init.init_command)
|
|
26
|
+
app.add_typer(config.app, name="config")
|
|
27
|
+
app.add_typer(self.app, name="self")
|
|
28
|
+
app.add_typer(libraries.app, name="libraries")
|
|
29
|
+
app.command("engine")(engine.engine_command)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.callback()
|
|
33
|
+
def main(
|
|
34
|
+
ctx: typer.Context,
|
|
35
|
+
no_update: bool = typer.Option( # noqa: FBT001
|
|
36
|
+
False, "--no-update", help="Skip the auto-update check."
|
|
37
|
+
),
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Griptape Nodes Engine CLI."""
|
|
40
|
+
if ctx.invoked_subcommand is None:
|
|
41
|
+
# Default to engine command when no subcommand is specified
|
|
42
|
+
engine.engine_command(no_update=no_update)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
app()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Shared constants and managers for CLI commands."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from xdg_base_dirs import xdg_config_home, xdg_data_home
|
|
10
|
+
|
|
11
|
+
from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
|
|
12
|
+
from griptape_nodes.retained_mode.managers.os_manager import OSManager
|
|
13
|
+
from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class InitConfig:
|
|
18
|
+
"""Configuration for initialization."""
|
|
19
|
+
|
|
20
|
+
interactive: bool = True
|
|
21
|
+
workspace_directory: str | None = None
|
|
22
|
+
api_key: str | None = None
|
|
23
|
+
storage_backend: str | None = None
|
|
24
|
+
register_advanced_library: bool | None = None
|
|
25
|
+
config_values: dict[str, Any] | None = None
|
|
26
|
+
secret_values: dict[str, str] | None = None
|
|
27
|
+
libraries_sync: bool | None = None
|
|
28
|
+
bucket_name: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Initialize console
|
|
32
|
+
console = Console()
|
|
33
|
+
|
|
34
|
+
# Directory paths
|
|
35
|
+
CONFIG_DIR = xdg_config_home() / "griptape_nodes"
|
|
36
|
+
DATA_DIR = xdg_data_home() / "griptape_nodes"
|
|
37
|
+
ENV_FILE = CONFIG_DIR / ".env"
|
|
38
|
+
CONFIG_FILE = CONFIG_DIR / "griptape_nodes_config.json"
|
|
39
|
+
|
|
40
|
+
# URLs and constants
|
|
41
|
+
LATEST_TAG = "latest"
|
|
42
|
+
PACKAGE_NAME = "griptape-nodes"
|
|
43
|
+
NODES_APP_URL = "https://nodes.griptape.ai"
|
|
44
|
+
NODES_TARBALL_URL = "https://github.com/griptape-ai/griptape-nodes/archive/refs/tags/{tag}.tar.gz"
|
|
45
|
+
PYPI_UPDATE_URL = "https://pypi.org/pypi/{package}/json"
|
|
46
|
+
GITHUB_UPDATE_URL = "https://api.github.com/repos/griptape-ai/{package}/git/refs/tags/{revision}"
|
|
47
|
+
GT_CLOUD_BASE_URL = os.getenv("GT_CLOUD_BASE_URL", "https://cloud.griptape.ai")
|
|
48
|
+
|
|
49
|
+
# Environment variable defaults for init configuration
|
|
50
|
+
ENV_WORKSPACE_DIRECTORY = os.getenv("GTN_WORKSPACE_DIRECTORY")
|
|
51
|
+
ENV_API_KEY = os.getenv("GTN_API_KEY")
|
|
52
|
+
ENV_STORAGE_BACKEND = os.getenv("GTN_STORAGE_BACKEND")
|
|
53
|
+
ENV_REGISTER_ADVANCED_LIBRARY = (
|
|
54
|
+
os.getenv("GTN_REGISTER_ADVANCED_LIBRARY", "false").lower() == "true"
|
|
55
|
+
if os.getenv("GTN_REGISTER_ADVANCED_LIBRARY") is not None
|
|
56
|
+
else None
|
|
57
|
+
)
|
|
58
|
+
ENV_LIBRARIES_SYNC = (
|
|
59
|
+
os.getenv("GTN_LIBRARIES_SYNC", "false").lower() == "true" if os.getenv("GTN_LIBRARIES_SYNC") is not None else None
|
|
60
|
+
)
|
|
61
|
+
ENV_GTN_BUCKET_NAME = os.getenv("GTN_BUCKET_NAME")
|
|
62
|
+
ENV_LIBRARIES_BASE_DIR = os.getenv("GTN_LIBRARIES_BASE_DIR", str(DATA_DIR / "libraries"))
|
|
63
|
+
|
|
64
|
+
# Initialize managers
|
|
65
|
+
config_manager = ConfigManager()
|
|
66
|
+
secrets_manager = SecretsManager(config_manager)
|
|
67
|
+
os_manager = OSManager()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def init_system_config() -> None:
|
|
71
|
+
"""Initializes the system config directory if it doesn't exist."""
|
|
72
|
+
if not CONFIG_DIR.exists():
|
|
73
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
|
|
75
|
+
files_to_create = [
|
|
76
|
+
(ENV_FILE, ""),
|
|
77
|
+
(CONFIG_FILE, "{}"),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
for file_name in files_to_create:
|
|
81
|
+
file_path = CONFIG_DIR / file_name[0]
|
|
82
|
+
if not file_path.exists():
|
|
83
|
+
with Path.open(file_path, "w", encoding="utf-8") as file:
|
|
84
|
+
file.write(file_name[1])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Common package."""
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger("griptape_nodes")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DirectedGraph:
|
|
9
|
+
"""Directed graph implementation using Python's graphlib for DAG operations."""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self._nodes: set[str] = set()
|
|
13
|
+
self._predecessors: dict[str, set[str]] = {}
|
|
14
|
+
|
|
15
|
+
def add_node(self, node_for_adding: str) -> None:
|
|
16
|
+
"""Add a node to the graph."""
|
|
17
|
+
self._nodes.add(node_for_adding)
|
|
18
|
+
if node_for_adding not in self._predecessors:
|
|
19
|
+
self._predecessors[node_for_adding] = set()
|
|
20
|
+
|
|
21
|
+
def add_edge(self, from_node: str, to_node: str) -> None:
|
|
22
|
+
"""Add a directed edge from from_node to to_node."""
|
|
23
|
+
self.add_node(from_node)
|
|
24
|
+
self.add_node(to_node)
|
|
25
|
+
self._predecessors[to_node].add(from_node)
|
|
26
|
+
|
|
27
|
+
def nodes(self) -> set[str]:
|
|
28
|
+
"""Return all nodes in the graph."""
|
|
29
|
+
return self._nodes.copy()
|
|
30
|
+
|
|
31
|
+
def in_degree(self, node: str) -> int:
|
|
32
|
+
"""Return the in-degree of a node (number of incoming edges)."""
|
|
33
|
+
if node not in self._nodes:
|
|
34
|
+
return 0
|
|
35
|
+
return len(self._predecessors.get(node, set()))
|
|
36
|
+
|
|
37
|
+
def remove_node(self, node: str) -> None:
|
|
38
|
+
"""Remove a node and all its edges from the graph."""
|
|
39
|
+
if node not in self._nodes:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
self._nodes.remove(node)
|
|
43
|
+
|
|
44
|
+
# Remove this node from all predecessor lists
|
|
45
|
+
for predecessors in self._predecessors.values():
|
|
46
|
+
predecessors.discard(node)
|
|
47
|
+
|
|
48
|
+
# Remove this node's predecessor entry
|
|
49
|
+
if node in self._predecessors:
|
|
50
|
+
del self._predecessors[node]
|
|
51
|
+
|
|
52
|
+
def clear(self) -> None:
|
|
53
|
+
"""Clear all nodes and edges from the graph."""
|
|
54
|
+
self._nodes.clear()
|
|
55
|
+
self._predecessors.clear()
|
|
@@ -4,8 +4,6 @@ from urllib.parse import urljoin
|
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
|
|
7
|
-
from griptape_nodes.app.api import STATIC_SERVER_HOST, STATIC_SERVER_PORT, STATIC_SERVER_URL
|
|
8
|
-
from griptape_nodes.app.app import STATIC_SERVER_ENABLED
|
|
9
7
|
from griptape_nodes.drivers.storage.base_storage_driver import BaseStorageDriver, CreateSignedUploadUrlResponse
|
|
10
8
|
|
|
11
9
|
logger = logging.getLogger("griptape_nodes")
|
|
@@ -20,6 +18,13 @@ class LocalStorageDriver(BaseStorageDriver):
|
|
|
20
18
|
Args:
|
|
21
19
|
base_url: The base URL for the static file server. If not provided, it will be constructed
|
|
22
20
|
"""
|
|
21
|
+
from griptape_nodes.app.api import (
|
|
22
|
+
STATIC_SERVER_ENABLED,
|
|
23
|
+
STATIC_SERVER_HOST,
|
|
24
|
+
STATIC_SERVER_PORT,
|
|
25
|
+
STATIC_SERVER_URL,
|
|
26
|
+
)
|
|
27
|
+
|
|
23
28
|
if not STATIC_SERVER_ENABLED:
|
|
24
29
|
msg = "Static server is not enabled. Please set STATIC_SERVER_ENABLED to True."
|
|
25
30
|
raise ValueError(msg)
|
|
@@ -4,15 +4,48 @@ import uuid
|
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
5
|
from copy import deepcopy
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
|
-
from enum import Enum, auto
|
|
7
|
+
from enum import Enum, StrEnum, auto
|
|
8
8
|
from typing import TYPE_CHECKING, Any, ClassVar, Literal, NamedTuple, Self, TypeVar
|
|
9
9
|
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NodeMessagePayload(BaseModel):
|
|
14
|
+
"""Structured payload for node messages.
|
|
15
|
+
|
|
16
|
+
This replaces the use of Any in message payloads, providing
|
|
17
|
+
better type safety and validation for node message handling.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
data: Any = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class NodeMessageResult(BaseModel):
|
|
24
|
+
"""Result from a node message callback.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
success: True if the message was handled successfully, False otherwise
|
|
28
|
+
details: Human-readable description of what happened
|
|
29
|
+
response: Optional response data to return to the sender
|
|
30
|
+
altered_workflow_state: True if the message handling altered workflow state.
|
|
31
|
+
Clients can use this to determine if the workflow needs to be re-saved.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
success: bool
|
|
35
|
+
details: str
|
|
36
|
+
response: NodeMessagePayload | None = None
|
|
37
|
+
altered_workflow_state: bool = True
|
|
38
|
+
|
|
39
|
+
|
|
10
40
|
if TYPE_CHECKING:
|
|
11
41
|
from collections.abc import Callable
|
|
12
42
|
from types import TracebackType
|
|
13
43
|
|
|
14
44
|
from griptape_nodes.exe_types.node_types import BaseNode
|
|
15
45
|
|
|
46
|
+
# Type alias for element message callback functions
|
|
47
|
+
type ElementMessageCallback = Callable[[str, "NodeMessagePayload | None"], "NodeMessageResult"]
|
|
48
|
+
|
|
16
49
|
T = TypeVar("T", bound="Parameter")
|
|
17
50
|
N = TypeVar("N", bound="BaseNodeElement")
|
|
18
51
|
|
|
@@ -24,7 +57,7 @@ class ParameterMode(Enum):
|
|
|
24
57
|
PROPERTY = auto()
|
|
25
58
|
|
|
26
59
|
|
|
27
|
-
class ParameterTypeBuiltin(
|
|
60
|
+
class ParameterTypeBuiltin(StrEnum):
|
|
28
61
|
STR = "str"
|
|
29
62
|
BOOL = "bool"
|
|
30
63
|
INT = "int"
|
|
@@ -416,6 +449,31 @@ class BaseNodeElement:
|
|
|
416
449
|
}
|
|
417
450
|
return event_data
|
|
418
451
|
|
|
452
|
+
def on_message_received(self, message_type: str, message: NodeMessagePayload | None) -> NodeMessageResult | None:
|
|
453
|
+
"""Virtual method for handling messages sent to this element.
|
|
454
|
+
|
|
455
|
+
Attempts to delegate to child elements first. If any child handles the message
|
|
456
|
+
(returns non-None), that result is returned immediately. Otherwise, falls back
|
|
457
|
+
to default behavior (return None).
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
message_type: String indicating the message type for parsing
|
|
461
|
+
message: Message payload as NodeMessagePayload or None
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
NodeMessageResult | None: Result if handled, None if no handler available
|
|
465
|
+
"""
|
|
466
|
+
# Try to delegate to all children first
|
|
467
|
+
# NOTE: This returns immediately on the first child that accepts the message (returns non-None).
|
|
468
|
+
# In the future, we may need to expand this to handle multiple children processing the same message.
|
|
469
|
+
for child in self._children:
|
|
470
|
+
result = child.on_message_received(message_type, message)
|
|
471
|
+
if result is not None:
|
|
472
|
+
return result
|
|
473
|
+
|
|
474
|
+
# No child handled it, return None (indicating no handler)
|
|
475
|
+
return None
|
|
476
|
+
|
|
419
477
|
|
|
420
478
|
class UIOptionsMixin:
|
|
421
479
|
"""Mixin providing UI options update functionality for classes with ui_options."""
|
|
@@ -6,12 +6,13 @@ from abc import ABC, abstractmethod
|
|
|
6
6
|
from collections.abc import Callable, Generator, Iterable
|
|
7
7
|
from concurrent.futures import ThreadPoolExecutor
|
|
8
8
|
from enum import StrEnum, auto
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
10
10
|
|
|
11
11
|
from griptape_nodes.exe_types.core_types import (
|
|
12
12
|
BaseNodeElement,
|
|
13
13
|
ControlParameterInput,
|
|
14
14
|
ControlParameterOutput,
|
|
15
|
+
NodeMessageResult,
|
|
15
16
|
Parameter,
|
|
16
17
|
ParameterContainer,
|
|
17
18
|
ParameterDictionary,
|
|
@@ -39,6 +40,9 @@ from griptape_nodes.retained_mode.events.parameter_events import (
|
|
|
39
40
|
)
|
|
40
41
|
from griptape_nodes.traits.options import Options
|
|
41
42
|
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from griptape_nodes.exe_types.core_types import NodeMessagePayload
|
|
45
|
+
|
|
42
46
|
logger = logging.getLogger("griptape_nodes")
|
|
43
47
|
|
|
44
48
|
T = TypeVar("T")
|
|
@@ -54,23 +58,6 @@ class NodeResolutionState(StrEnum):
|
|
|
54
58
|
RESOLVED = auto()
|
|
55
59
|
|
|
56
60
|
|
|
57
|
-
class NodeMessageResult(NamedTuple):
|
|
58
|
-
"""Result from a node message callback.
|
|
59
|
-
|
|
60
|
-
Attributes:
|
|
61
|
-
success: True if the message was handled successfully, False otherwise
|
|
62
|
-
details: Human-readable description of what happened
|
|
63
|
-
response: Optional response data to return to the sender
|
|
64
|
-
altered_workflow_state: True if the message handling altered workflow state.
|
|
65
|
-
Clients can use this to determine if the workflow needs to be re-saved.
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
success: bool
|
|
69
|
-
details: str
|
|
70
|
-
response: Any = None
|
|
71
|
-
altered_workflow_state: bool = True
|
|
72
|
-
|
|
73
|
-
|
|
74
61
|
class BaseNode(ABC):
|
|
75
62
|
# Owned by a flow
|
|
76
63
|
name: str
|
|
@@ -80,7 +67,7 @@ class BaseNode(ABC):
|
|
|
80
67
|
state: NodeResolutionState
|
|
81
68
|
current_spotlight_parameter: Parameter | None = None
|
|
82
69
|
parameter_values: dict[str, Any]
|
|
83
|
-
parameter_output_values:
|
|
70
|
+
parameter_output_values: TrackedParameterOutputValues
|
|
84
71
|
stop_flow: bool = False
|
|
85
72
|
root_ui_element: BaseNodeElement
|
|
86
73
|
_tracked_parameters: list[BaseNodeElement]
|
|
@@ -289,15 +276,18 @@ class BaseNode(ABC):
|
|
|
289
276
|
|
|
290
277
|
def on_node_message_received(
|
|
291
278
|
self,
|
|
292
|
-
optional_element_name: str | None,
|
|
279
|
+
optional_element_name: str | None,
|
|
293
280
|
message_type: str,
|
|
294
|
-
message:
|
|
281
|
+
message: NodeMessagePayload | None,
|
|
295
282
|
) -> NodeMessageResult:
|
|
296
283
|
"""Callback for when a message is sent directly to this node.
|
|
297
284
|
|
|
298
285
|
Custom nodes may elect to override this method to handle specific message types
|
|
299
286
|
and implement custom communication patterns with external systems.
|
|
300
287
|
|
|
288
|
+
If optional_element_name is provided, this method will attempt to find the
|
|
289
|
+
element and delegate the message handling to that element's on_message_received method.
|
|
290
|
+
|
|
301
291
|
Args:
|
|
302
292
|
optional_element_name: Optional element name this message relates to
|
|
303
293
|
message_type: String indicating the message type for parsing
|
|
@@ -306,6 +296,26 @@ class BaseNode(ABC):
|
|
|
306
296
|
Returns:
|
|
307
297
|
NodeMessageResult: Result containing success status, details, and optional response
|
|
308
298
|
"""
|
|
299
|
+
# If optional_element_name is provided, delegate to the specific element
|
|
300
|
+
if optional_element_name is not None:
|
|
301
|
+
element = self.root_ui_element.find_element_by_name(optional_element_name)
|
|
302
|
+
if element is None:
|
|
303
|
+
return NodeMessageResult(
|
|
304
|
+
success=False,
|
|
305
|
+
details=f"Node '{self.name}' received message for element '{optional_element_name}' but no element with that name was found",
|
|
306
|
+
response=None,
|
|
307
|
+
)
|
|
308
|
+
# Delegate to the element's message handler
|
|
309
|
+
result = element.on_message_received(message_type, message)
|
|
310
|
+
if result is None:
|
|
311
|
+
return NodeMessageResult(
|
|
312
|
+
success=False,
|
|
313
|
+
details=f"Element '{optional_element_name}' received message type '{message_type}' but no handler was available",
|
|
314
|
+
response=None,
|
|
315
|
+
)
|
|
316
|
+
return result
|
|
317
|
+
|
|
318
|
+
# If no element name specified, fall back to node-level handling
|
|
309
319
|
return NodeMessageResult(
|
|
310
320
|
success=False,
|
|
311
321
|
details=f"Node '{self.name}' was sent a message of type '{message_type}'. Failed because no message handler was specified for this node. Implement the on_node_message_received method in this node class in order for it to receive messages.",
|
|
@@ -620,10 +630,10 @@ class BaseNode(ABC):
|
|
|
620
630
|
# Allow custom node logic to prepare and possibly mutate the value before it is actually set.
|
|
621
631
|
# Record any parameters modified for cascading.
|
|
622
632
|
if not initial_setup:
|
|
623
|
-
if
|
|
624
|
-
final_value = self.before_value_set(parameter=parameter, value=candidate_value)
|
|
625
|
-
else:
|
|
633
|
+
if skip_before_value_set:
|
|
626
634
|
final_value = candidate_value
|
|
635
|
+
else:
|
|
636
|
+
final_value = self.before_value_set(parameter=parameter, value=candidate_value)
|
|
627
637
|
# ACTUALLY SET THE NEW VALUE
|
|
628
638
|
self.parameter_values[param_name] = final_value
|
|
629
639
|
|
|
@@ -1097,6 +1107,10 @@ class TrackedParameterOutputValues(dict[str, Any]):
|
|
|
1097
1107
|
for key in keys_to_clear:
|
|
1098
1108
|
self._emit_parameter_change_event(key, None, deleted=True)
|
|
1099
1109
|
|
|
1110
|
+
def silent_clear(self) -> None:
|
|
1111
|
+
"""Clear all values without emitting parameter change events."""
|
|
1112
|
+
super().clear()
|
|
1113
|
+
|
|
1100
1114
|
def update(self, *args, **kwargs) -> None:
|
|
1101
1115
|
# Handle both dict.update(other) and dict.update(**kwargs) patterns
|
|
1102
1116
|
if args:
|