griptape-nodes 0.55.1__py3-none-any.whl → 0.56.1__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/app/app.py +10 -15
- griptape_nodes/app/watch.py +35 -67
- griptape_nodes/bootstrap/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/utils/python_subprocess_executor.py +122 -0
- griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +418 -0
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +37 -8
- griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +326 -0
- griptape_nodes/bootstrap/workflow_executors/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +51 -0
- griptape_nodes/bootstrap/workflow_publishers/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +43 -0
- griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +84 -0
- griptape_nodes/bootstrap/workflow_publishers/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +54 -0
- griptape_nodes/cli/commands/engine.py +4 -15
- griptape_nodes/cli/commands/init.py +88 -0
- griptape_nodes/cli/commands/models.py +2 -0
- griptape_nodes/cli/main.py +6 -1
- griptape_nodes/cli/shared.py +1 -0
- griptape_nodes/exe_types/core_types.py +130 -0
- griptape_nodes/exe_types/node_types.py +125 -13
- griptape_nodes/machines/control_flow.py +10 -0
- griptape_nodes/machines/dag_builder.py +21 -2
- griptape_nodes/machines/parallel_resolution.py +25 -10
- griptape_nodes/node_library/workflow_registry.py +73 -3
- griptape_nodes/retained_mode/events/agent_events.py +2 -0
- griptape_nodes/retained_mode/events/base_events.py +18 -17
- griptape_nodes/retained_mode/events/execution_events.py +15 -3
- griptape_nodes/retained_mode/events/flow_events.py +63 -7
- griptape_nodes/retained_mode/events/mcp_events.py +363 -0
- griptape_nodes/retained_mode/events/node_events.py +3 -4
- griptape_nodes/retained_mode/events/resource_events.py +290 -0
- griptape_nodes/retained_mode/events/workflow_events.py +57 -2
- griptape_nodes/retained_mode/griptape_nodes.py +17 -1
- griptape_nodes/retained_mode/managers/agent_manager.py +67 -4
- griptape_nodes/retained_mode/managers/event_manager.py +31 -13
- griptape_nodes/retained_mode/managers/flow_manager.py +731 -33
- griptape_nodes/retained_mode/managers/library_manager.py +15 -23
- griptape_nodes/retained_mode/managers/mcp_manager.py +364 -0
- griptape_nodes/retained_mode/managers/model_manager.py +184 -83
- griptape_nodes/retained_mode/managers/node_manager.py +15 -4
- griptape_nodes/retained_mode/managers/os_manager.py +118 -1
- griptape_nodes/retained_mode/managers/resource_components/__init__.py +1 -0
- griptape_nodes/retained_mode/managers/resource_components/capability_field.py +41 -0
- griptape_nodes/retained_mode/managers/resource_components/comparator.py +18 -0
- griptape_nodes/retained_mode/managers/resource_components/resource_instance.py +236 -0
- griptape_nodes/retained_mode/managers/resource_components/resource_type.py +79 -0
- griptape_nodes/retained_mode/managers/resource_manager.py +306 -0
- griptape_nodes/retained_mode/managers/resource_types/__init__.py +1 -0
- griptape_nodes/retained_mode/managers/resource_types/cpu_resource.py +108 -0
- griptape_nodes/retained_mode/managers/resource_types/os_resource.py +87 -0
- griptape_nodes/retained_mode/managers/settings.py +45 -0
- griptape_nodes/retained_mode/managers/sync_manager.py +10 -3
- griptape_nodes/retained_mode/managers/workflow_manager.py +447 -263
- griptape_nodes/traits/multi_options.py +5 -1
- griptape_nodes/traits/options.py +10 -2
- {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/METADATA +2 -2
- {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/RECORD +60 -37
- {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/entry_points.txt +0 -0
|
@@ -55,6 +55,10 @@ def init_command( # noqa: PLR0913
|
|
|
55
55
|
bool,
|
|
56
56
|
typer.Option(help="Run init in non-interactive mode (no prompts)."),
|
|
57
57
|
] = False,
|
|
58
|
+
hf_token: Annotated[
|
|
59
|
+
str | None,
|
|
60
|
+
typer.Option(help="Set the Hugging Face token for downloading gated models."),
|
|
61
|
+
] = None,
|
|
58
62
|
config: Annotated[
|
|
59
63
|
list[str] | None,
|
|
60
64
|
typer.Option(
|
|
@@ -83,6 +87,7 @@ def init_command( # noqa: PLR0913
|
|
|
83
87
|
secret_values=secret_values,
|
|
84
88
|
libraries_sync=libraries_sync,
|
|
85
89
|
bucket_name=bucket_name,
|
|
90
|
+
hf_token=hf_token,
|
|
86
91
|
)
|
|
87
92
|
)
|
|
88
93
|
|
|
@@ -128,6 +133,7 @@ def _run_init_configuration(config: InitConfig) -> None:
|
|
|
128
133
|
_handle_workspace_config(config)
|
|
129
134
|
_handle_storage_backend_config(config)
|
|
130
135
|
_handle_bucket_config(config)
|
|
136
|
+
_handle_hf_token_config(config)
|
|
131
137
|
_handle_advanced_library_config(config)
|
|
132
138
|
_handle_arbitrary_configs(config)
|
|
133
139
|
|
|
@@ -193,6 +199,25 @@ def _handle_bucket_config(config: InitConfig) -> str | None:
|
|
|
193
199
|
return bucket_id
|
|
194
200
|
|
|
195
201
|
|
|
202
|
+
def _handle_hf_token_config(config: InitConfig) -> str | None:
|
|
203
|
+
"""Handle Hugging Face token configuration step."""
|
|
204
|
+
hf_token = None
|
|
205
|
+
|
|
206
|
+
if config.interactive:
|
|
207
|
+
# First ask if they want to configure an HF token
|
|
208
|
+
configure_hf_token = _prompt_for_hf_token_configuration()
|
|
209
|
+
if configure_hf_token:
|
|
210
|
+
hf_token = _prompt_for_hf_token(default_hf_token=config.hf_token)
|
|
211
|
+
elif config.hf_token is not None:
|
|
212
|
+
hf_token = config.hf_token
|
|
213
|
+
|
|
214
|
+
if hf_token is not None:
|
|
215
|
+
secrets_manager.set_secret("HF_TOKEN", hf_token)
|
|
216
|
+
console.print("[bold green]Hugging Face token set[/bold green]")
|
|
217
|
+
|
|
218
|
+
return hf_token
|
|
219
|
+
|
|
220
|
+
|
|
196
221
|
def _handle_advanced_library_config(config: InitConfig) -> bool | None:
|
|
197
222
|
"""Handle advanced library configuration step."""
|
|
198
223
|
register_advanced_library = config.register_advanced_library
|
|
@@ -513,6 +538,69 @@ def _create_new_bucket(bucket_name: str) -> str:
|
|
|
513
538
|
return bucket_id
|
|
514
539
|
|
|
515
540
|
|
|
541
|
+
def _prompt_for_hf_token_configuration() -> bool:
|
|
542
|
+
"""Prompts the user whether to configure a Hugging Face token."""
|
|
543
|
+
# Check if there's already an HF token configured
|
|
544
|
+
current_hf_token = secrets_manager.get_secret("HF_TOKEN", should_error_on_not_found=False)
|
|
545
|
+
|
|
546
|
+
if current_hf_token:
|
|
547
|
+
explainer = """[bold cyan]Hugging Face Token Configuration[/bold cyan]
|
|
548
|
+
You currently have a Hugging Face token configured.
|
|
549
|
+
|
|
550
|
+
Hugging Face tokens are used to access gated models from the Hugging Face Hub, such as:
|
|
551
|
+
- Meta's Llama models
|
|
552
|
+
- black-forest-labs/FLUX.1-dev
|
|
553
|
+
- Other restricted or premium models
|
|
554
|
+
|
|
555
|
+
Would you like to update your Hugging Face token or keep the current one?"""
|
|
556
|
+
prompt_text = "Update Hugging Face token?"
|
|
557
|
+
default_value = False
|
|
558
|
+
else:
|
|
559
|
+
explainer = """[bold cyan]Hugging Face Token Configuration[/bold cyan]
|
|
560
|
+
Would you like to configure a Hugging Face token?
|
|
561
|
+
|
|
562
|
+
Hugging Face tokens are used by the model manager to download gated models from the Hugging Face Hub, such as:
|
|
563
|
+
- Meta's Llama models
|
|
564
|
+
- black-forest-labs/FLUX.1-dev
|
|
565
|
+
- Other restricted or premium models
|
|
566
|
+
|
|
567
|
+
If you don't plan to use gated models, you can skip this step.
|
|
568
|
+
You can get a token from https://huggingface.co/settings/tokens
|
|
569
|
+
|
|
570
|
+
You can always configure a token later by running the initialization process again."""
|
|
571
|
+
prompt_text = "Configure Hugging Face token?"
|
|
572
|
+
default_value = False
|
|
573
|
+
|
|
574
|
+
console.print(Panel(explainer, expand=False))
|
|
575
|
+
return Confirm.ask(prompt_text, default=default_value)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _prompt_for_hf_token(default_hf_token: str | None = None) -> str | None:
|
|
579
|
+
"""Prompts the user for their Hugging Face token."""
|
|
580
|
+
if default_hf_token is None:
|
|
581
|
+
default_hf_token = secrets_manager.get_secret("HF_TOKEN", should_error_on_not_found=False)
|
|
582
|
+
|
|
583
|
+
explainer = """[bold cyan]Hugging Face Token[/bold cyan]
|
|
584
|
+
Please enter your Hugging Face token to enable downloading of gated models.
|
|
585
|
+
|
|
586
|
+
To get a token:
|
|
587
|
+
1. Go to https://huggingface.co/settings/tokens
|
|
588
|
+
2. Create a new token with 'Read' permissions
|
|
589
|
+
3. Copy and paste the token here
|
|
590
|
+
|
|
591
|
+
You can leave this blank to skip token configuration."""
|
|
592
|
+
console.print(Panel(explainer, expand=False))
|
|
593
|
+
|
|
594
|
+
hf_token = Prompt.ask(
|
|
595
|
+
"Hugging Face Token (optional)",
|
|
596
|
+
default=default_hf_token or "",
|
|
597
|
+
show_default=False,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
# Return None if empty string
|
|
601
|
+
return hf_token if hf_token.strip() else None
|
|
602
|
+
|
|
603
|
+
|
|
516
604
|
def _parse_key_value_pairs(pairs: list[str] | None) -> dict[str, Any] | None:
|
|
517
605
|
"""Parse key=value pairs from a list of strings.
|
|
518
606
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Models command for managing AI models."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import sys
|
|
4
5
|
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
import typer
|
|
@@ -130,6 +131,7 @@ async def _download_model(
|
|
|
130
131
|
except Exception as e:
|
|
131
132
|
console.print("[bold red]Model download failed:[/bold red]")
|
|
132
133
|
console.print(f"[red]{e}[/red]")
|
|
134
|
+
sys.exit(1)
|
|
133
135
|
|
|
134
136
|
|
|
135
137
|
async def _list_models() -> None:
|
griptape_nodes/cli/main.py
CHANGED
|
@@ -10,6 +10,7 @@ from rich.console import Console
|
|
|
10
10
|
sys.path.append(str(Path.cwd()))
|
|
11
11
|
|
|
12
12
|
from griptape_nodes.cli.commands import config, engine, init, libraries, models, self
|
|
13
|
+
from griptape_nodes.cli.commands.engine import _auto_update_self
|
|
13
14
|
from griptape_nodes.utils.version_utils import get_complete_version_string
|
|
14
15
|
|
|
15
16
|
console = Console()
|
|
@@ -47,9 +48,13 @@ def main(
|
|
|
47
48
|
console.print(f"[bold green]{version_string}[/bold green]")
|
|
48
49
|
raise typer.Exit
|
|
49
50
|
|
|
51
|
+
# Run auto-update check for any command (unless disabled)
|
|
52
|
+
if not no_update:
|
|
53
|
+
_auto_update_self()
|
|
54
|
+
|
|
50
55
|
if ctx.invoked_subcommand is None:
|
|
51
56
|
# Default to engine command when no subcommand is specified
|
|
52
|
-
engine.engine_command(
|
|
57
|
+
engine.engine_command()
|
|
53
58
|
|
|
54
59
|
|
|
55
60
|
if __name__ == "__main__":
|
griptape_nodes/cli/shared.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import uuid
|
|
4
5
|
from abc import ABC, abstractmethod
|
|
5
6
|
from copy import deepcopy
|
|
@@ -9,6 +10,8 @@ from typing import TYPE_CHECKING, Any, ClassVar, Literal, NamedTuple, Self, Type
|
|
|
9
10
|
|
|
10
11
|
from pydantic import BaseModel
|
|
11
12
|
|
|
13
|
+
logger = logging.getLogger("griptape_nodes")
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
class NodeMessagePayload(BaseModel):
|
|
14
17
|
"""Structured payload for node messages.
|
|
@@ -554,6 +557,7 @@ class ParameterMessage(BaseNodeElement, UIOptionsMixin):
|
|
|
554
557
|
button_align: ButtonAlignType = "full-width",
|
|
555
558
|
full_width: bool = False,
|
|
556
559
|
ui_options: dict | None = None,
|
|
560
|
+
traits: set[Trait.__class__ | Trait] | None = None,
|
|
557
561
|
**kwargs,
|
|
558
562
|
):
|
|
559
563
|
super().__init__(element_type=ParameterMessage.__name__, **kwargs)
|
|
@@ -569,6 +573,17 @@ class ParameterMessage(BaseNodeElement, UIOptionsMixin):
|
|
|
569
573
|
self._full_width = full_width
|
|
570
574
|
self._ui_options = ui_options or {}
|
|
571
575
|
|
|
576
|
+
# Handle traits if provided
|
|
577
|
+
if traits:
|
|
578
|
+
for trait in traits:
|
|
579
|
+
if isinstance(trait, type):
|
|
580
|
+
# It's a trait class, instantiate it
|
|
581
|
+
trait_instance = trait()
|
|
582
|
+
else:
|
|
583
|
+
# It's already a trait instance
|
|
584
|
+
trait_instance = trait
|
|
585
|
+
self.add_child(trait_instance)
|
|
586
|
+
|
|
572
587
|
@property
|
|
573
588
|
def variant(self) -> VariantType:
|
|
574
589
|
return self._variant
|
|
@@ -694,6 +709,16 @@ class ParameterMessage(BaseNodeElement, UIOptionsMixin):
|
|
|
694
709
|
else:
|
|
695
710
|
button_icon = self.button_icon
|
|
696
711
|
|
|
712
|
+
# Check if there are any Button traits with on_click callbacks
|
|
713
|
+
has_button_callback = False
|
|
714
|
+
for child in self.children:
|
|
715
|
+
# Import here to avoid circular imports
|
|
716
|
+
from griptape_nodes.traits.button import Button
|
|
717
|
+
|
|
718
|
+
if isinstance(child, Button) and child.on_click_callback is not None:
|
|
719
|
+
has_button_callback = True
|
|
720
|
+
break
|
|
721
|
+
|
|
697
722
|
# Merge the UI options with the message-specific options
|
|
698
723
|
# Always include these fields, even if they're None or empty
|
|
699
724
|
message_ui_options = {
|
|
@@ -705,6 +730,7 @@ class ParameterMessage(BaseNodeElement, UIOptionsMixin):
|
|
|
705
730
|
"button_icon": button_icon,
|
|
706
731
|
"button_variant": self.button_variant,
|
|
707
732
|
"button_align": self.button_align,
|
|
733
|
+
"button_on_click": has_button_callback,
|
|
708
734
|
"full_width": self.full_width,
|
|
709
735
|
}
|
|
710
736
|
|
|
@@ -1483,12 +1509,23 @@ class ParameterList(ParameterContainer):
|
|
|
1483
1509
|
user_defined: bool = False,
|
|
1484
1510
|
element_id: str | None = None,
|
|
1485
1511
|
element_type: str | None = None,
|
|
1512
|
+
# UI convenience parameters
|
|
1513
|
+
collapsed: bool | None = None,
|
|
1514
|
+
child_prefix: str | None = None,
|
|
1515
|
+
grid: bool | None = None,
|
|
1516
|
+
grid_columns: int | None = None,
|
|
1486
1517
|
):
|
|
1487
1518
|
if traits:
|
|
1488
1519
|
self._original_traits = traits
|
|
1489
1520
|
else:
|
|
1490
1521
|
self._original_traits = set()
|
|
1491
1522
|
|
|
1523
|
+
# Store the UI convenience parameters
|
|
1524
|
+
self._collapsed = collapsed
|
|
1525
|
+
self._child_prefix = child_prefix
|
|
1526
|
+
self._grid = grid
|
|
1527
|
+
self._grid_columns = grid_columns
|
|
1528
|
+
|
|
1492
1529
|
# Remember: we're a Parameter, too, just like everybody else.
|
|
1493
1530
|
super().__init__(
|
|
1494
1531
|
name=name,
|
|
@@ -1511,6 +1548,99 @@ class ParameterList(ParameterContainer):
|
|
|
1511
1548
|
element_type=element_type,
|
|
1512
1549
|
)
|
|
1513
1550
|
|
|
1551
|
+
@property
|
|
1552
|
+
def collapsed(self) -> bool | None:
|
|
1553
|
+
return self._collapsed
|
|
1554
|
+
|
|
1555
|
+
@collapsed.setter
|
|
1556
|
+
@BaseNodeElement.emits_update_on_write
|
|
1557
|
+
def collapsed(self, value: bool | None) -> None:
|
|
1558
|
+
self._collapsed = value
|
|
1559
|
+
|
|
1560
|
+
@property
|
|
1561
|
+
def child_prefix(self) -> str | None:
|
|
1562
|
+
return self._child_prefix
|
|
1563
|
+
|
|
1564
|
+
@child_prefix.setter
|
|
1565
|
+
@BaseNodeElement.emits_update_on_write
|
|
1566
|
+
def child_prefix(self, value: str | None) -> None:
|
|
1567
|
+
self._child_prefix = value
|
|
1568
|
+
|
|
1569
|
+
@property
|
|
1570
|
+
def grid(self) -> bool | None:
|
|
1571
|
+
return self._grid
|
|
1572
|
+
|
|
1573
|
+
@grid.setter
|
|
1574
|
+
@BaseNodeElement.emits_update_on_write
|
|
1575
|
+
def grid(self, value: bool | None) -> None:
|
|
1576
|
+
self._grid = value
|
|
1577
|
+
|
|
1578
|
+
@property
|
|
1579
|
+
def grid_columns(self) -> int | None:
|
|
1580
|
+
return self._grid_columns
|
|
1581
|
+
|
|
1582
|
+
@grid_columns.setter
|
|
1583
|
+
@BaseNodeElement.emits_update_on_write
|
|
1584
|
+
def grid_columns(self, value: int | None) -> None:
|
|
1585
|
+
self._grid_columns = value
|
|
1586
|
+
|
|
1587
|
+
@property
|
|
1588
|
+
def ui_options(self) -> dict:
|
|
1589
|
+
"""Override ui_options to merge convenience parameters in real-time."""
|
|
1590
|
+
# Get base ui_options from parent
|
|
1591
|
+
base_ui_options = super().ui_options
|
|
1592
|
+
|
|
1593
|
+
# Build convenience options from instance parameters
|
|
1594
|
+
convenience_options = {}
|
|
1595
|
+
|
|
1596
|
+
if self._collapsed is not None:
|
|
1597
|
+
convenience_options["collapsed"] = self._collapsed
|
|
1598
|
+
|
|
1599
|
+
if self._child_prefix is not None:
|
|
1600
|
+
convenience_options["child_prefix"] = self._child_prefix
|
|
1601
|
+
|
|
1602
|
+
if self._grid is not None and self._grid:
|
|
1603
|
+
convenience_options["display"] = "grid"
|
|
1604
|
+
|
|
1605
|
+
if self._grid_columns is not None and self._grid:
|
|
1606
|
+
convenience_options["columns"] = self._grid_columns
|
|
1607
|
+
|
|
1608
|
+
# Merge convenience options with base ui_options
|
|
1609
|
+
return {
|
|
1610
|
+
**base_ui_options,
|
|
1611
|
+
**convenience_options,
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
@ui_options.setter
|
|
1615
|
+
@BaseNodeElement.emits_update_on_write
|
|
1616
|
+
def ui_options(self, value: dict) -> None:
|
|
1617
|
+
"""Set ui_options, preserving convenience parameters."""
|
|
1618
|
+
# Extract convenience parameters from the incoming value
|
|
1619
|
+
if "display" in value and value["display"] == "grid":
|
|
1620
|
+
self._grid = True
|
|
1621
|
+
if "columns" in value:
|
|
1622
|
+
self._grid_columns = value["columns"]
|
|
1623
|
+
else:
|
|
1624
|
+
self._grid = False
|
|
1625
|
+
|
|
1626
|
+
if "collapsed" in value:
|
|
1627
|
+
self._collapsed = value["collapsed"]
|
|
1628
|
+
|
|
1629
|
+
if "child_prefix" in value:
|
|
1630
|
+
self._child_prefix = value["child_prefix"]
|
|
1631
|
+
|
|
1632
|
+
# Set the base ui_options (excluding convenience parameters)
|
|
1633
|
+
base_ui_options = {
|
|
1634
|
+
k: v for k, v in value.items() if k not in ["display", "columns", "collapsed", "child_prefix"]
|
|
1635
|
+
}
|
|
1636
|
+
self._ui_options = base_ui_options
|
|
1637
|
+
|
|
1638
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1639
|
+
"""Override to_dict to use the merged ui_options."""
|
|
1640
|
+
data = super().to_dict()
|
|
1641
|
+
data["ui_options"] = self.ui_options
|
|
1642
|
+
return data
|
|
1643
|
+
|
|
1514
1644
|
def _custom_getter_for_property_type(self) -> str:
|
|
1515
1645
|
base_type = super()._custom_getter_for_property_type()
|
|
1516
1646
|
result = f"list[{base_type}]"
|
|
@@ -5,8 +5,9 @@ import logging
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
6
|
from collections.abc import Callable, Generator, Iterable
|
|
7
7
|
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
from dataclasses import dataclass, field
|
|
8
9
|
from enum import StrEnum, auto
|
|
9
|
-
from typing import TYPE_CHECKING, Any, TypeVar
|
|
10
|
+
from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar
|
|
10
11
|
|
|
11
12
|
from griptape_nodes.exe_types.core_types import (
|
|
12
13
|
BaseNodeElement,
|
|
@@ -43,6 +44,7 @@ from griptape_nodes.traits.options import Options
|
|
|
43
44
|
|
|
44
45
|
if TYPE_CHECKING:
|
|
45
46
|
from griptape_nodes.exe_types.core_types import NodeMessagePayload
|
|
47
|
+
from griptape_nodes.node_library.library_registry import LibraryNameAndVersion
|
|
46
48
|
|
|
47
49
|
logger = logging.getLogger("griptape_nodes")
|
|
48
50
|
|
|
@@ -50,6 +52,53 @@ T = TypeVar("T")
|
|
|
50
52
|
|
|
51
53
|
AsyncResult = Generator[Callable[[], T], T]
|
|
52
54
|
|
|
55
|
+
LOCAL_EXECUTION = "Local Execution"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ImportDependency(NamedTuple):
|
|
59
|
+
"""Import dependency specification for a node.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
module: The module name to import
|
|
63
|
+
class_name: Optional class name to import from the module. If None, imports the entire module.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
module: str
|
|
67
|
+
class_name: str | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class NodeDependencies:
|
|
72
|
+
"""Dependencies that a node has on external resources.
|
|
73
|
+
|
|
74
|
+
This class provides a way for nodes to declare their dependencies on workflows,
|
|
75
|
+
static files, Python imports, and libraries. This information can be used by the system
|
|
76
|
+
for workflow packaging, dependency resolution, and deployment planning.
|
|
77
|
+
|
|
78
|
+
Attributes:
|
|
79
|
+
referenced_workflows: Set of workflow names that this node references
|
|
80
|
+
static_files: Set of static file names that this node depends on
|
|
81
|
+
imports: Set of Python imports that this node requires
|
|
82
|
+
libraries: Set of library names and versions that this node uses
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
referenced_workflows: set[str] = field(default_factory=set)
|
|
86
|
+
static_files: set[str] = field(default_factory=set)
|
|
87
|
+
imports: set[ImportDependency] = field(default_factory=set)
|
|
88
|
+
libraries: set[LibraryNameAndVersion] = field(default_factory=set)
|
|
89
|
+
|
|
90
|
+
def aggregate_from(self, other: NodeDependencies) -> None:
|
|
91
|
+
"""Aggregate dependencies from another NodeDependencies object into this one.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
other: The NodeDependencies object to aggregate from
|
|
95
|
+
"""
|
|
96
|
+
# Aggregate all dependency types - no None checks needed since we use default_factory=set
|
|
97
|
+
self.referenced_workflows.update(other.referenced_workflows)
|
|
98
|
+
self.static_files.update(other.static_files)
|
|
99
|
+
self.imports.update(other.imports)
|
|
100
|
+
self.libraries.update(other.libraries)
|
|
101
|
+
|
|
53
102
|
|
|
54
103
|
class NodeResolutionState(StrEnum):
|
|
55
104
|
"""Possible states for a node during resolution."""
|
|
@@ -59,6 +108,23 @@ class NodeResolutionState(StrEnum):
|
|
|
59
108
|
RESOLVED = auto()
|
|
60
109
|
|
|
61
110
|
|
|
111
|
+
def get_library_names_with_publish_handlers() -> list[str]:
|
|
112
|
+
"""Get names of all registered libraries that have PublishWorkflowRequest handlers."""
|
|
113
|
+
from griptape_nodes.retained_mode.events.workflow_events import PublishWorkflowRequest
|
|
114
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
115
|
+
|
|
116
|
+
library_manager = GriptapeNodes.LibraryManager()
|
|
117
|
+
event_handlers = library_manager.get_registered_event_handlers(PublishWorkflowRequest)
|
|
118
|
+
|
|
119
|
+
# Always include "local" as the first option
|
|
120
|
+
library_names = [LOCAL_EXECUTION]
|
|
121
|
+
|
|
122
|
+
# Add all registered library names that can handle PublishWorkflowRequest
|
|
123
|
+
library_names.extend(sorted(event_handlers.keys()))
|
|
124
|
+
|
|
125
|
+
return library_names
|
|
126
|
+
|
|
127
|
+
|
|
62
128
|
class BaseNode(ABC):
|
|
63
129
|
# Owned by a flow
|
|
64
130
|
name: str
|
|
@@ -104,6 +170,16 @@ class BaseNode(ABC):
|
|
|
104
170
|
self.process_generator = None
|
|
105
171
|
self._tracked_parameters = []
|
|
106
172
|
self.set_entry_control_parameter(None)
|
|
173
|
+
self.execution_environment = Parameter(
|
|
174
|
+
name="execution_environment",
|
|
175
|
+
tooltip="Environment that the node should execute in",
|
|
176
|
+
type=ParameterTypeBuiltin.STR,
|
|
177
|
+
allowed_modes={ParameterMode.PROPERTY},
|
|
178
|
+
default_value=LOCAL_EXECUTION,
|
|
179
|
+
traits={Options(choices=get_library_names_with_publish_handlers())},
|
|
180
|
+
ui_options={"hide": True},
|
|
181
|
+
)
|
|
182
|
+
self.add_parameter(self.execution_environment)
|
|
107
183
|
|
|
108
184
|
# This is gross and we need to have a universal pass on resolution state changes and emission of events. That's what this ticket does!
|
|
109
185
|
# https://github.com/griptape-ai/griptape-nodes/issues/994
|
|
@@ -384,9 +460,7 @@ class BaseNode(ABC):
|
|
|
384
460
|
for name in names:
|
|
385
461
|
parameter = self.get_parameter_by_name(name)
|
|
386
462
|
if parameter is not None:
|
|
387
|
-
ui_options = parameter.ui_options
|
|
388
|
-
ui_options["hide"] = not visible
|
|
389
|
-
parameter.ui_options = ui_options
|
|
463
|
+
parameter.ui_options = {**parameter.ui_options, "hide": not visible}
|
|
390
464
|
|
|
391
465
|
def get_message_by_name_or_element_id(self, element: str) -> ParameterMessage | None:
|
|
392
466
|
element_items = self.root_ui_element.find_elements_by_type(ParameterMessage)
|
|
@@ -408,9 +482,7 @@ class BaseNode(ABC):
|
|
|
408
482
|
for name in names:
|
|
409
483
|
message = self.get_message_by_name_or_element_id(name)
|
|
410
484
|
if message is not None:
|
|
411
|
-
ui_options = message.ui_options
|
|
412
|
-
ui_options["hide"] = not visible
|
|
413
|
-
message.ui_options = ui_options
|
|
485
|
+
message.ui_options = {**message.ui_options, "hide": not visible}
|
|
414
486
|
|
|
415
487
|
def hide_message_by_name(self, names: str | list[str]) -> None:
|
|
416
488
|
self._set_message_visibility(names, visible=False)
|
|
@@ -727,11 +799,9 @@ class BaseNode(ABC):
|
|
|
727
799
|
return param
|
|
728
800
|
return None
|
|
729
801
|
|
|
730
|
-
# Abstract method to process the node. Must be defined by the type
|
|
731
802
|
# Must save the values of the output parameters in NodeContext.
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
pass
|
|
803
|
+
def process(self) -> AsyncResult | None:
|
|
804
|
+
raise NotImplementedError
|
|
735
805
|
|
|
736
806
|
async def aprocess(self) -> None:
|
|
737
807
|
"""Async version of process().
|
|
@@ -825,6 +895,35 @@ class BaseNode(ABC):
|
|
|
825
895
|
# Then clear the reference to the first spotlight parameter
|
|
826
896
|
self.current_spotlight_parameter = None
|
|
827
897
|
|
|
898
|
+
def get_node_dependencies(self) -> NodeDependencies | None:
|
|
899
|
+
"""Return the dependencies that this node has on external resources.
|
|
900
|
+
|
|
901
|
+
This method should be overridden by nodes that have dependencies on:
|
|
902
|
+
- Referenced workflows: Other workflows that this node calls or references
|
|
903
|
+
- Static files: Files that this node reads from or requires for operation
|
|
904
|
+
- Python imports: Modules or classes that this node imports beyond standard dependencies
|
|
905
|
+
|
|
906
|
+
This information can be used by the system for workflow packaging, dependency
|
|
907
|
+
resolution, deployment planning, and ensuring all required resources are available.
|
|
908
|
+
|
|
909
|
+
Returns:
|
|
910
|
+
NodeDependencies object containing the node's dependencies, or None if the node
|
|
911
|
+
has no external dependencies beyond the standard framework dependencies.
|
|
912
|
+
|
|
913
|
+
Example:
|
|
914
|
+
def get_node_dependencies(self) -> NodeDependencies | None:
|
|
915
|
+
return NodeDependencies(
|
|
916
|
+
referenced_workflows={"image_processing_workflow", "validation_workflow"},
|
|
917
|
+
static_files={"config.json", "model_weights.pkl"},
|
|
918
|
+
imports={
|
|
919
|
+
ImportDependency("numpy"),
|
|
920
|
+
ImportDependency("sklearn.linear_model", "LinearRegression"),
|
|
921
|
+
ImportDependency("custom_module", "SpecialProcessor")
|
|
922
|
+
}
|
|
923
|
+
)
|
|
924
|
+
"""
|
|
925
|
+
return None
|
|
926
|
+
|
|
828
927
|
def append_value_to_parameter(self, parameter_name: str, value: Any) -> None:
|
|
829
928
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
830
929
|
|
|
@@ -867,7 +966,7 @@ class BaseNode(ABC):
|
|
|
867
966
|
msg = f"Parameter '{parameter_name} doesn't exist on {self.name}'"
|
|
868
967
|
raise RuntimeError(msg)
|
|
869
968
|
|
|
870
|
-
def reorder_elements(self, element_order: list[str | int]) -> None:
|
|
969
|
+
def reorder_elements(self, element_order: list[str] | list[int] | list[str | int]) -> None:
|
|
871
970
|
"""Reorder the elements of this node.
|
|
872
971
|
|
|
873
972
|
Args:
|
|
@@ -1109,7 +1208,10 @@ class TrackedParameterOutputValues(dict[str, Any]):
|
|
|
1109
1208
|
keys_to_clear = list(self.keys())
|
|
1110
1209
|
super().clear()
|
|
1111
1210
|
for key in keys_to_clear:
|
|
1112
|
-
|
|
1211
|
+
# Some nodes still have values set, even if their output values are cleared
|
|
1212
|
+
# Here, we are emitting an event with those set values, to not misrepresent the values of the parameters in the UI.
|
|
1213
|
+
value = self._node.get_parameter_value(key)
|
|
1214
|
+
self._emit_parameter_change_event(key, value, deleted=True)
|
|
1113
1215
|
|
|
1114
1216
|
def silent_clear(self) -> None:
|
|
1115
1217
|
"""Clear all values without emitting parameter change events."""
|
|
@@ -1393,6 +1495,16 @@ class EndNode(BaseNode):
|
|
|
1393
1495
|
|
|
1394
1496
|
self.status_component.set_execution_result(was_successful=was_successful, result_details=details)
|
|
1395
1497
|
|
|
1498
|
+
# Update all values to use the output value
|
|
1499
|
+
for param in self.parameters:
|
|
1500
|
+
if param.type != ParameterTypeBuiltin.CONTROL_TYPE:
|
|
1501
|
+
value = self.get_parameter_value(param.name)
|
|
1502
|
+
self.parameter_output_values[param.name] = value
|
|
1503
|
+
next_control_output = self.get_next_control_output()
|
|
1504
|
+
# Update which control parameter to flag as the output value.
|
|
1505
|
+
if next_control_output is not None:
|
|
1506
|
+
self.parameter_output_values[next_control_output.name] = 1
|
|
1507
|
+
|
|
1396
1508
|
|
|
1397
1509
|
class StartLoopNode(BaseNode):
|
|
1398
1510
|
end_node: EndLoopNode | None = None
|
|
@@ -15,6 +15,7 @@ from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, Exec
|
|
|
15
15
|
from griptape_nodes.retained_mode.events.execution_events import (
|
|
16
16
|
ControlFlowResolvedEvent,
|
|
17
17
|
CurrentControlNodeEvent,
|
|
18
|
+
InvolvedNodesEvent,
|
|
18
19
|
SelectedControlOutputEvent,
|
|
19
20
|
)
|
|
20
21
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
@@ -252,12 +253,21 @@ class ControlFlowMachine(FSM[ControlFlowContext]):
|
|
|
252
253
|
current_nodes = await self._process_nodes_for_dag(start_node)
|
|
253
254
|
else:
|
|
254
255
|
current_nodes = [start_node]
|
|
256
|
+
# For control flow/sequential: emit all nodes in flow as involved
|
|
255
257
|
self._context.current_nodes = current_nodes
|
|
256
258
|
# Set entry control parameter for initial node (None for workflow start)
|
|
257
259
|
for node in current_nodes:
|
|
258
260
|
node.set_entry_control_parameter(None)
|
|
259
261
|
# Set up to debug
|
|
260
262
|
self._context.paused = debug_mode
|
|
263
|
+
flow_manager = GriptapeNodes.FlowManager()
|
|
264
|
+
flow = flow_manager.get_flow_by_name(self._context.flow_name)
|
|
265
|
+
involved_nodes = list(flow.nodes.keys())
|
|
266
|
+
GriptapeNodes.EventManager().put_event(
|
|
267
|
+
ExecutionGriptapeNodeEvent(
|
|
268
|
+
wrapped_event=ExecutionEvent(payload=InvolvedNodesEvent(involved_nodes=involved_nodes))
|
|
269
|
+
)
|
|
270
|
+
)
|
|
261
271
|
await self.start(ResolveNodeState) # Begins the flow
|
|
262
272
|
|
|
263
273
|
async def update(self) -> None:
|
|
@@ -43,10 +43,12 @@ class DagBuilder:
|
|
|
43
43
|
|
|
44
44
|
graphs: dict[str, DirectedGraph] # Str is the name of the start node associated here.
|
|
45
45
|
node_to_reference: dict[str, DagNode]
|
|
46
|
+
graph_to_nodes: dict[str, set[str]] # Track which nodes belong to which graph
|
|
46
47
|
|
|
47
48
|
def __init__(self) -> None:
|
|
48
49
|
self.graphs = {}
|
|
49
50
|
self.node_to_reference: dict[str, DagNode] = {}
|
|
51
|
+
self.graph_to_nodes = {}
|
|
50
52
|
|
|
51
53
|
# Complex with the inner recursive method, but it needs connections and added_nodes.
|
|
52
54
|
def add_node_with_dependencies(self, node: BaseNode, graph_name: str = "default") -> list[BaseNode]: # noqa: C901
|
|
@@ -59,16 +61,15 @@ class DagBuilder:
|
|
|
59
61
|
if graph is None:
|
|
60
62
|
graph = DirectedGraph()
|
|
61
63
|
self.graphs[graph_name] = graph
|
|
64
|
+
self.graph_to_nodes[graph_name] = set()
|
|
62
65
|
|
|
63
66
|
def _add_node_recursive(current_node: BaseNode, visited: set[str], graph: DirectedGraph) -> None:
|
|
64
67
|
if current_node.name in visited:
|
|
65
68
|
return
|
|
66
69
|
visited.add(current_node.name)
|
|
67
|
-
|
|
68
70
|
# Skip if already in DAG (use DAG membership, not resolved state)
|
|
69
71
|
if current_node.name in self.node_to_reference:
|
|
70
72
|
return
|
|
71
|
-
|
|
72
73
|
# Process dependencies first (depth-first)
|
|
73
74
|
ignore_data_dependencies = False
|
|
74
75
|
# This is specifically for output_selector. Overriding 'initialize_spotlight' doesn't work anymore.
|
|
@@ -98,6 +99,10 @@ class DagBuilder:
|
|
|
98
99
|
dag_node = DagNode(node_reference=current_node, node_state=NodeState.WAITING)
|
|
99
100
|
self.node_to_reference[current_node.name] = dag_node
|
|
100
101
|
graph.add_node(node_for_adding=current_node.name)
|
|
102
|
+
|
|
103
|
+
# Track which nodes belong to this graph
|
|
104
|
+
self.graph_to_nodes[graph_name].add(current_node.name)
|
|
105
|
+
|
|
101
106
|
# DON'T mark as resolved - that happens during actual execution
|
|
102
107
|
added_nodes.append(current_node)
|
|
103
108
|
|
|
@@ -117,12 +122,19 @@ class DagBuilder:
|
|
|
117
122
|
graph = DirectedGraph()
|
|
118
123
|
self.graphs[graph_name] = graph
|
|
119
124
|
graph.add_node(node_for_adding=node.name)
|
|
125
|
+
|
|
126
|
+
# Track which nodes belong to this graph
|
|
127
|
+
if graph_name not in self.graph_to_nodes:
|
|
128
|
+
self.graph_to_nodes[graph_name] = set()
|
|
129
|
+
self.graph_to_nodes[graph_name].add(node.name)
|
|
130
|
+
|
|
120
131
|
return dag_node
|
|
121
132
|
|
|
122
133
|
def clear(self) -> None:
|
|
123
134
|
"""Clear all nodes and references from the DAG builder."""
|
|
124
135
|
self.graphs.clear()
|
|
125
136
|
self.node_to_reference.clear()
|
|
137
|
+
self.graph_to_nodes.clear()
|
|
126
138
|
|
|
127
139
|
def can_queue_control_node(self, node: DagNode) -> bool:
|
|
128
140
|
if len(self.graphs) == 1:
|
|
@@ -205,3 +217,10 @@ class DagBuilder:
|
|
|
205
217
|
return True
|
|
206
218
|
|
|
207
219
|
return False
|
|
220
|
+
|
|
221
|
+
def cleanup_empty_graph_nodes(self, graph_name: str) -> None:
|
|
222
|
+
"""Remove nodes from node_to_reference when their graph becomes empty (only in single node resolution)."""
|
|
223
|
+
if graph_name in self.graph_to_nodes:
|
|
224
|
+
for node_name in self.graph_to_nodes[graph_name]:
|
|
225
|
+
self.node_to_reference.pop(node_name, None)
|
|
226
|
+
self.graph_to_nodes.pop(graph_name, None)
|