griptape-nodes 0.36.2__py3-none-any.whl → 0.37.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/__init__.py +68 -2
- griptape_nodes/app/__init__.py +1 -1
- griptape_nodes/app/app.py +6 -6
- griptape_nodes/app/app_websocket.py +8 -8
- griptape_nodes/exe_types/connections.py +32 -1
- griptape_nodes/exe_types/core_types.py +16 -0
- griptape_nodes/exe_types/flow.py +2 -2
- griptape_nodes/exe_types/node_types.py +207 -0
- griptape_nodes/retained_mode/events/agent_events.py +83 -0
- griptape_nodes/retained_mode/griptape_nodes.py +8 -0
- griptape_nodes/retained_mode/managers/agent_manager.py +103 -0
- griptape_nodes/retained_mode/managers/library_manager.py +50 -37
- griptape_nodes/retained_mode/managers/node_manager.py +29 -2
- griptape_nodes/retained_mode/retained_mode.py +662 -18
- {griptape_nodes-0.36.2.dist-info → griptape_nodes-0.37.1.dist-info}/METADATA +3 -2
- {griptape_nodes-0.36.2.dist-info → griptape_nodes-0.37.1.dist-info}/RECORD +19 -17
- {griptape_nodes-0.36.2.dist-info → griptape_nodes-0.37.1.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.36.2.dist-info → griptape_nodes-0.37.1.dist-info}/entry_points.txt +0 -0
- {griptape_nodes-0.36.2.dist-info → griptape_nodes-0.37.1.dist-info}/licenses/LICENSE +0 -0
griptape_nodes/__init__.py
CHANGED
|
@@ -15,7 +15,7 @@ with console.status("Loading Griptape Nodes...") as status:
|
|
|
15
15
|
import tarfile
|
|
16
16
|
import tempfile
|
|
17
17
|
from pathlib import Path
|
|
18
|
-
from typing import Literal
|
|
18
|
+
from typing import Any, Literal
|
|
19
19
|
|
|
20
20
|
import httpx
|
|
21
21
|
from dotenv import load_dotenv
|
|
@@ -75,7 +75,7 @@ def main() -> None:
|
|
|
75
75
|
_process_args(args)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def _run_init( # noqa: PLR0913
|
|
78
|
+
def _run_init( # noqa: PLR0913, C901
|
|
79
79
|
*,
|
|
80
80
|
interactive: bool = True,
|
|
81
81
|
workspace_directory: str | None = None,
|
|
@@ -83,6 +83,8 @@ def _run_init( # noqa: PLR0913
|
|
|
83
83
|
storage_backend: str | None = None,
|
|
84
84
|
storage_backend_bucket_id: str | None = None,
|
|
85
85
|
register_advanced_library: bool | None = None,
|
|
86
|
+
config_values: dict[str, Any] | None = None,
|
|
87
|
+
secret_values: dict[str, str] | None = None,
|
|
86
88
|
) -> None:
|
|
87
89
|
"""Runs through the engine init steps.
|
|
88
90
|
|
|
@@ -93,6 +95,8 @@ def _run_init( # noqa: PLR0913
|
|
|
93
95
|
storage_backend (str | None): The storage backend to set.
|
|
94
96
|
storage_backend_bucket_id (str | None): The storage backend bucket ID to set.
|
|
95
97
|
register_advanced_library (bool | None): Whether to register the advanced library.
|
|
98
|
+
config_values (dict[str, any] | None): Arbitrary config key-value pairs to set.
|
|
99
|
+
secret_values (dict[str, str] | None): Arbitrary secret key-value pairs to set.
|
|
96
100
|
"""
|
|
97
101
|
__init_system_config()
|
|
98
102
|
|
|
@@ -132,6 +136,18 @@ def _run_init( # noqa: PLR0913
|
|
|
132
136
|
)
|
|
133
137
|
console.print(f"[bold green]Libraries to register set to: {', '.join(libraries_to_register)}[/bold green]")
|
|
134
138
|
|
|
139
|
+
# Set arbitrary config values
|
|
140
|
+
if config_values:
|
|
141
|
+
for key, value in config_values.items():
|
|
142
|
+
config_manager.set_config_value(key, value)
|
|
143
|
+
console.print(f"[bold green]Config '{key}' set to: {value}[/bold green]")
|
|
144
|
+
|
|
145
|
+
# Set arbitrary secret values
|
|
146
|
+
if secret_values:
|
|
147
|
+
for key, value in secret_values.items():
|
|
148
|
+
secrets_manager.set_secret(key, value)
|
|
149
|
+
console.print(f"[bold green]Secret '{key}' set[/bold green]")
|
|
150
|
+
|
|
135
151
|
_sync_assets()
|
|
136
152
|
console.print("[bold green]Initialization complete![/bold green]")
|
|
137
153
|
|
|
@@ -152,6 +168,8 @@ def _start_engine(*, no_update: bool = False) -> None:
|
|
|
152
168
|
storage_backend_bucket_id=ENV_STORAGE_BACKEND_BUCKET_ID,
|
|
153
169
|
register_advanced_library=ENV_REGISTER_ADVANCED_LIBRARY,
|
|
154
170
|
interactive=True,
|
|
171
|
+
config_values=None,
|
|
172
|
+
secret_values=None,
|
|
155
173
|
)
|
|
156
174
|
|
|
157
175
|
# Confusing double negation -- If `no_update` is set, we want to skip the update
|
|
@@ -214,6 +232,18 @@ def _get_args() -> argparse.Namespace:
|
|
|
214
232
|
action="store_true",
|
|
215
233
|
help="Run init in non-interactive mode (no prompts).",
|
|
216
234
|
)
|
|
235
|
+
init_parser.add_argument(
|
|
236
|
+
"--config",
|
|
237
|
+
action="append",
|
|
238
|
+
metavar="KEY=VALUE",
|
|
239
|
+
help="Set arbitrary config values as key=value pairs (can be used multiple times). Example: --config log_level=DEBUG --config workspace_directory=/tmp",
|
|
240
|
+
)
|
|
241
|
+
init_parser.add_argument(
|
|
242
|
+
"--secret",
|
|
243
|
+
action="append",
|
|
244
|
+
metavar="KEY=VALUE",
|
|
245
|
+
help="Set arbitrary secret values as key=value pairs (can be used multiple times). Example: --secret MY_API_KEY=abc123 --secret OTHER_KEY=xyz789",
|
|
246
|
+
)
|
|
217
247
|
|
|
218
248
|
# engine
|
|
219
249
|
subparsers.add_parser("engine", help="Run the Griptape Nodes engine.")
|
|
@@ -635,8 +665,42 @@ def _uninstall_self() -> None:
|
|
|
635
665
|
os_manager.replace_process(["uv", "tool", "uninstall", "griptape-nodes"])
|
|
636
666
|
|
|
637
667
|
|
|
668
|
+
def _parse_key_value_pairs(pairs: list[str] | None) -> dict[str, str] | None:
|
|
669
|
+
"""Parse key=value pairs from a list of strings.
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
pairs: List of strings in the format "key=value"
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
Dictionary of key-value pairs, or None if no pairs provided
|
|
676
|
+
"""
|
|
677
|
+
if not pairs:
|
|
678
|
+
return None
|
|
679
|
+
|
|
680
|
+
result = {}
|
|
681
|
+
for pair in pairs:
|
|
682
|
+
if "=" not in pair:
|
|
683
|
+
console.print(f"[bold red]Invalid key=value pair: {pair}. Expected format: key=value[/bold red]")
|
|
684
|
+
continue
|
|
685
|
+
# Split only on the first = to handle values that contain =
|
|
686
|
+
key, value = pair.split("=", 1)
|
|
687
|
+
key = key.strip()
|
|
688
|
+
value = value.strip()
|
|
689
|
+
|
|
690
|
+
if not key:
|
|
691
|
+
console.print(f"[bold red]Empty key in pair: {pair}[/bold red]")
|
|
692
|
+
continue
|
|
693
|
+
|
|
694
|
+
result[key] = value
|
|
695
|
+
|
|
696
|
+
return result if result else None
|
|
697
|
+
|
|
698
|
+
|
|
638
699
|
def _process_args(args: argparse.Namespace) -> None: # noqa: C901, PLR0912
|
|
639
700
|
if args.command == "init":
|
|
701
|
+
config_values = _parse_key_value_pairs(getattr(args, "config", None))
|
|
702
|
+
secret_values = _parse_key_value_pairs(getattr(args, "secret", None))
|
|
703
|
+
|
|
640
704
|
_run_init(
|
|
641
705
|
interactive=not args.no_interactive,
|
|
642
706
|
workspace_directory=args.workspace_directory,
|
|
@@ -644,6 +708,8 @@ def _process_args(args: argparse.Namespace) -> None: # noqa: C901, PLR0912
|
|
|
644
708
|
storage_backend=args.storage_backend,
|
|
645
709
|
storage_backend_bucket_id=args.storage_backend_bucket_id,
|
|
646
710
|
register_advanced_library=args.register_advanced_library,
|
|
711
|
+
config_values=config_values,
|
|
712
|
+
secret_values=secret_values,
|
|
647
713
|
)
|
|
648
714
|
elif args.command == "engine":
|
|
649
715
|
_start_engine(no_update=args.no_update)
|
griptape_nodes/app/__init__.py
CHANGED
griptape_nodes/app/app.py
CHANGED
|
@@ -76,12 +76,6 @@ class EventLogHandler(logging.Handler):
|
|
|
76
76
|
)
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
griptape_nodes_logger = logging.getLogger("griptape_nodes")
|
|
80
|
-
# When running as an app, we want to forward all log messages to the event queue so they can be sent to the GUI
|
|
81
|
-
griptape_nodes_logger.addHandler(EventLogHandler())
|
|
82
|
-
griptape_nodes_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
|
|
83
|
-
griptape_nodes_logger.setLevel(logging.INFO)
|
|
84
|
-
|
|
85
79
|
# Logger for this module. Important that this is not the same as the griptape_nodes logger or else we'll have infinite log events.
|
|
86
80
|
logger = logging.getLogger(__name__)
|
|
87
81
|
console = Console()
|
|
@@ -94,6 +88,12 @@ def start_app() -> None:
|
|
|
94
88
|
"""
|
|
95
89
|
global socket_manager # noqa: PLW0603 # Need to initialize the socket lazily here to avoid auth-ing too early
|
|
96
90
|
|
|
91
|
+
griptape_nodes_logger = logging.getLogger("griptape_nodes")
|
|
92
|
+
# When running as an app, we want to forward all log messages to the event queue so they can be sent to the GUI
|
|
93
|
+
griptape_nodes_logger.addHandler(EventLogHandler())
|
|
94
|
+
griptape_nodes_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
|
|
95
|
+
griptape_nodes_logger.setLevel(logging.INFO)
|
|
96
|
+
|
|
97
97
|
# Listen for SSE events from the Nodes API in a separate thread
|
|
98
98
|
socket_manager = NodesApiSocketManager()
|
|
99
99
|
|
|
@@ -79,12 +79,6 @@ class EventLogHandler(logging.Handler):
|
|
|
79
79
|
)
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
griptape_nodes_logger = logging.getLogger("griptape_nodes")
|
|
83
|
-
# When running as an app, we want to forward all log messages to the event queue so they can be sent to the GUI
|
|
84
|
-
griptape_nodes_logger.addHandler(EventLogHandler())
|
|
85
|
-
griptape_nodes_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
|
|
86
|
-
griptape_nodes_logger.setLevel(logging.INFO)
|
|
87
|
-
|
|
88
82
|
# Logger for this module. Important that this is not the same as the griptape_nodes logger or else we'll have infinite log events.
|
|
89
83
|
logger = logging.getLogger("griptape_nodes_app")
|
|
90
84
|
console = Console()
|
|
@@ -97,6 +91,12 @@ def start_app() -> None:
|
|
|
97
91
|
"""
|
|
98
92
|
_init_event_listeners()
|
|
99
93
|
|
|
94
|
+
griptape_nodes_logger = logging.getLogger("griptape_nodes")
|
|
95
|
+
# When running as an app, we want to forward all log messages to the event queue so they can be sent to the GUI
|
|
96
|
+
griptape_nodes_logger.addHandler(EventLogHandler())
|
|
97
|
+
griptape_nodes_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
|
|
98
|
+
griptape_nodes_logger.setLevel(logging.INFO)
|
|
99
|
+
|
|
100
100
|
# Listen for any signals to exit the app
|
|
101
101
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
102
102
|
signal.signal(sig, lambda *_: sys.exit(0))
|
|
@@ -123,8 +123,8 @@ def _serve_static_server() -> None:
|
|
|
123
123
|
app.add_middleware(
|
|
124
124
|
CORSMiddleware,
|
|
125
125
|
allow_origins=[
|
|
126
|
-
os.getenv("GRIPTAPE_NODES_UI_BASE_URL", "https://nodes.griptape.ai"),
|
|
127
|
-
"https://nodes-staging.griptape.ai",
|
|
126
|
+
os.getenv("GRIPTAPE_NODES_UI_BASE_URL", "https://app.nodes.griptape.ai"),
|
|
127
|
+
"https://app.nodes-staging.griptape.ai",
|
|
128
128
|
"http://localhost:5173",
|
|
129
129
|
],
|
|
130
130
|
allow_credentials=True,
|
|
@@ -2,7 +2,7 @@ import logging
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
|
|
4
4
|
from griptape_nodes.exe_types.core_types import Parameter, ParameterMode, ParameterTypeBuiltin
|
|
5
|
-
from griptape_nodes.exe_types.node_types import BaseNode, Connection, NodeResolutionState
|
|
5
|
+
from griptape_nodes.exe_types.node_types import BaseNode, Connection, EndLoopNode, NodeResolutionState, StartLoopNode
|
|
6
6
|
|
|
7
7
|
logger = logging.getLogger("griptape_nodes")
|
|
8
8
|
|
|
@@ -99,8 +99,39 @@ class Connections:
|
|
|
99
99
|
)
|
|
100
100
|
return connection is None
|
|
101
101
|
|
|
102
|
+
def _get_connected_node_for_end_loop_control(
|
|
103
|
+
self, end_loop_node: EndLoopNode, control_parameter: Parameter
|
|
104
|
+
) -> tuple[BaseNode, Parameter] | None:
|
|
105
|
+
"""For an EndLoopNode and its control parameter, finds the connected node and parameter.
|
|
106
|
+
|
|
107
|
+
It checks both outgoing connections (where EndLoopNode's parameter is a source)
|
|
108
|
+
and incoming connections (where EndLoopNode's parameter is a target).
|
|
109
|
+
"""
|
|
110
|
+
# Check if the EndLoopNode's control parameter is a source for an outgoing connection
|
|
111
|
+
if ParameterMode.OUTPUT in control_parameter.allowed_modes:
|
|
112
|
+
outgoing_connections_for_node = self.outgoing_index.get(end_loop_node.name, {})
|
|
113
|
+
connection_ids_as_source = outgoing_connections_for_node.get(control_parameter.name, [])
|
|
114
|
+
if connection_ids_as_source:
|
|
115
|
+
connection_id = connection_ids_as_source[0]
|
|
116
|
+
connection = self.connections.get(connection_id)
|
|
117
|
+
if connection:
|
|
118
|
+
return connection.target_node, connection.target_parameter
|
|
119
|
+
elif ParameterMode.INPUT in control_parameter.allowed_modes:
|
|
120
|
+
# Check if the EndLoopNode's control parameter is a target for an incoming connection
|
|
121
|
+
incoming_connections_for_node = self.incoming_index.get(end_loop_node.name, {})
|
|
122
|
+
connection_ids_as_target = incoming_connections_for_node.get(control_parameter.name, [])
|
|
123
|
+
if connection_ids_as_target:
|
|
124
|
+
for connection_id in connection_ids_as_target:
|
|
125
|
+
connection = self.connections.get(connection_id)
|
|
126
|
+
if connection and isinstance(connection.source_node, StartLoopNode):
|
|
127
|
+
return connection.source_node, connection.source_parameter
|
|
128
|
+
return None # No connection found for this control parameter
|
|
129
|
+
|
|
102
130
|
def get_connected_node(self, node: BaseNode, parameter: Parameter) -> tuple[BaseNode, Parameter] | None:
|
|
103
131
|
# Check to see if we should be getting the next connection or the previous connection based on the parameter.
|
|
132
|
+
# Override this method for EndLoopNodes - these might have to go backwards or forwards.
|
|
133
|
+
if isinstance(node, EndLoopNode) and ParameterTypeBuiltin.CONTROL_TYPE.value == parameter.output_type:
|
|
134
|
+
return self._get_connected_node_for_end_loop_control(node, parameter)
|
|
104
135
|
if ParameterTypeBuiltin.CONTROL_TYPE.value == parameter.output_type:
|
|
105
136
|
connections = self.outgoing_index
|
|
106
137
|
else:
|
|
@@ -163,6 +163,7 @@ class BaseNodeElement:
|
|
|
163
163
|
element_id: str = field(default_factory=lambda: str(uuid.uuid4().hex))
|
|
164
164
|
element_type: str = field(default_factory=lambda: BaseNodeElement.__name__)
|
|
165
165
|
name: str = field(default_factory=lambda: str(f"{BaseNodeElement.__name__}_{uuid.uuid4().hex}"))
|
|
166
|
+
parent_group_name: str | None = None
|
|
166
167
|
|
|
167
168
|
_children: list[BaseNodeElement] = field(default_factory=list)
|
|
168
169
|
_stack: ClassVar[list[BaseNodeElement]] = []
|
|
@@ -219,6 +220,7 @@ class BaseNodeElement:
|
|
|
219
220
|
return {
|
|
220
221
|
"element_id": self.element_id,
|
|
221
222
|
"element_type": self.__class__.__name__,
|
|
223
|
+
"parent_group_name": self.parent_group_name,
|
|
222
224
|
"children": [child.to_dict() for child in self._children],
|
|
223
225
|
}
|
|
224
226
|
|
|
@@ -409,6 +411,20 @@ class ParameterGroup(BaseNodeElement):
|
|
|
409
411
|
differences[key] = other_value
|
|
410
412
|
return differences
|
|
411
413
|
|
|
414
|
+
def add_child(self, child: BaseNodeElement) -> None:
|
|
415
|
+
child.parent_group_name = self.name
|
|
416
|
+
return super().add_child(child)
|
|
417
|
+
|
|
418
|
+
def remove_child(self, child: BaseNodeElement | str) -> None:
|
|
419
|
+
if isinstance(child, str):
|
|
420
|
+
child_from_str = self.find_element_by_name(child)
|
|
421
|
+
if child_from_str is not None and isinstance(child_from_str, BaseNodeElement):
|
|
422
|
+
child_from_str.parent_group_name = None
|
|
423
|
+
return super().remove_child(child_from_str)
|
|
424
|
+
else:
|
|
425
|
+
child.parent_group_name = None
|
|
426
|
+
return super().remove_child(child)
|
|
427
|
+
|
|
412
428
|
|
|
413
429
|
# TODO: https://github.com/griptape-ai/griptape-nodes/issues/856
|
|
414
430
|
class ParameterBase(BaseNodeElement, ABC):
|
griptape_nodes/exe_types/flow.py
CHANGED
|
@@ -8,7 +8,7 @@ from griptape.events import EventBus
|
|
|
8
8
|
|
|
9
9
|
from griptape_nodes.exe_types.connections import Connections
|
|
10
10
|
from griptape_nodes.exe_types.core_types import ParameterTypeBuiltin
|
|
11
|
-
from griptape_nodes.exe_types.node_types import NodeResolutionState, StartNode
|
|
11
|
+
from griptape_nodes.exe_types.node_types import NodeResolutionState, StartLoopNode, StartNode
|
|
12
12
|
from griptape_nodes.machines.control_flow import CompleteState, ControlFlowMachine
|
|
13
13
|
from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, ExecutionGriptapeNodeEvent
|
|
14
14
|
from griptape_nodes.retained_mode.events.execution_events import ControlFlowCancelledEvent
|
|
@@ -313,7 +313,7 @@ class ControlFlow:
|
|
|
313
313
|
has_control_connection = True
|
|
314
314
|
break
|
|
315
315
|
# if there is a connection coming in, isn't a start.
|
|
316
|
-
if has_control_connection:
|
|
316
|
+
if has_control_connection and not isinstance(node, StartLoopNode):
|
|
317
317
|
continue
|
|
318
318
|
# Does it have an outgoing connection?
|
|
319
319
|
if node.name in cn_mgr.outgoing_index:
|
|
@@ -581,6 +581,201 @@ class BaseNode(ABC):
|
|
|
581
581
|
msg = f"Parameter '{parameter_name} doesn't exist on {self.name}'"
|
|
582
582
|
raise RuntimeError(msg)
|
|
583
583
|
|
|
584
|
+
def reorder_elements(self, element_order: list[str | int]) -> None:
|
|
585
|
+
"""Reorder the elements of this node.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
element_order: A list of element names or indices in the desired order.
|
|
589
|
+
Can mix names and indices. Names take precedence over indices.
|
|
590
|
+
|
|
591
|
+
Example:
|
|
592
|
+
# Reorder by names
|
|
593
|
+
node.reorder_elements(["element1", "element2", "element3"])
|
|
594
|
+
|
|
595
|
+
# Reorder by indices
|
|
596
|
+
node.reorder_elements([0, 2, 1])
|
|
597
|
+
|
|
598
|
+
# Mix names and indices
|
|
599
|
+
node.reorder_elements(["element1", 2, "element3"])
|
|
600
|
+
"""
|
|
601
|
+
# Get current elements
|
|
602
|
+
current_elements = self.root_ui_element._children
|
|
603
|
+
|
|
604
|
+
# Create a new ordered list of elements
|
|
605
|
+
ordered_elements = []
|
|
606
|
+
for item in element_order:
|
|
607
|
+
if isinstance(item, str):
|
|
608
|
+
# Find element by name
|
|
609
|
+
element = self.root_ui_element.find_element_by_name(item)
|
|
610
|
+
if element is None:
|
|
611
|
+
msg = f"Element '{item}' not found"
|
|
612
|
+
raise ValueError(msg)
|
|
613
|
+
ordered_elements.append(element)
|
|
614
|
+
elif isinstance(item, int):
|
|
615
|
+
# Get element by index
|
|
616
|
+
if item < 0 or item >= len(current_elements):
|
|
617
|
+
msg = f"Element index {item} out of range"
|
|
618
|
+
raise ValueError(msg)
|
|
619
|
+
ordered_elements.append(current_elements[item])
|
|
620
|
+
else:
|
|
621
|
+
msg = "Element order must contain strings (names) or integers (indices)"
|
|
622
|
+
raise TypeError(msg)
|
|
623
|
+
|
|
624
|
+
# Verify we have all elements
|
|
625
|
+
if len(ordered_elements) != len(current_elements):
|
|
626
|
+
msg = "Element order must include all elements exactly once"
|
|
627
|
+
raise ValueError(msg)
|
|
628
|
+
|
|
629
|
+
# Remove all elements from root_ui_element
|
|
630
|
+
for element in current_elements:
|
|
631
|
+
self.root_ui_element.remove_child(element)
|
|
632
|
+
|
|
633
|
+
# Add elements back in the new order
|
|
634
|
+
for element in ordered_elements:
|
|
635
|
+
self.root_ui_element.add_child(element)
|
|
636
|
+
|
|
637
|
+
def move_element_to_position(self, element: str | int, position: str | int) -> None:
|
|
638
|
+
"""Move a single element to a specific position in the element list.
|
|
639
|
+
|
|
640
|
+
Args:
|
|
641
|
+
element: The element to move, specified by name or index
|
|
642
|
+
position: The target position, which can be:
|
|
643
|
+
- "first" to move to the beginning
|
|
644
|
+
- "last" to move to the end
|
|
645
|
+
- An integer index (0-based) for a specific position
|
|
646
|
+
|
|
647
|
+
Example:
|
|
648
|
+
# Move element to first position by name
|
|
649
|
+
node.move_element_to_position("element1", "first")
|
|
650
|
+
|
|
651
|
+
# Move element to last position by index
|
|
652
|
+
node.move_element_to_position(0, "last")
|
|
653
|
+
|
|
654
|
+
# Move element to specific position
|
|
655
|
+
node.move_element_to_position("element1", 2)
|
|
656
|
+
"""
|
|
657
|
+
# Get list of all element names
|
|
658
|
+
element_names = [child.name for child in self.root_ui_element._children]
|
|
659
|
+
|
|
660
|
+
# Convert element index to name if needed
|
|
661
|
+
element = self._get_element_name(element, element_names)
|
|
662
|
+
|
|
663
|
+
# Create new order with moved element
|
|
664
|
+
new_order = element_names.copy()
|
|
665
|
+
idx = new_order.index(element)
|
|
666
|
+
|
|
667
|
+
# Handle special position values
|
|
668
|
+
if position == "first":
|
|
669
|
+
target_pos = 0
|
|
670
|
+
elif position == "last":
|
|
671
|
+
target_pos = len(new_order) - 1
|
|
672
|
+
elif isinstance(position, int):
|
|
673
|
+
if position < 0 or position >= len(new_order):
|
|
674
|
+
msg = f"Target position {position} out of range"
|
|
675
|
+
raise ValueError(msg)
|
|
676
|
+
target_pos = position
|
|
677
|
+
else:
|
|
678
|
+
msg = "Position must be 'first', 'last', or an integer index"
|
|
679
|
+
raise TypeError(msg)
|
|
680
|
+
|
|
681
|
+
# Remove element from current position and insert at target position
|
|
682
|
+
new_order.pop(idx)
|
|
683
|
+
new_order.insert(target_pos, element)
|
|
684
|
+
|
|
685
|
+
# Use reorder_elements to apply the move
|
|
686
|
+
self.reorder_elements(list(new_order))
|
|
687
|
+
|
|
688
|
+
def _get_element_name(self, element: str | int, element_names: list[str]) -> str:
|
|
689
|
+
"""Convert an element identifier (name or index) to its name.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
element: Element identifier, either a name (str) or index (int)
|
|
693
|
+
element_names: List of all element names
|
|
694
|
+
|
|
695
|
+
Returns:
|
|
696
|
+
The element name
|
|
697
|
+
|
|
698
|
+
Raises:
|
|
699
|
+
ValueError: If index is out of range
|
|
700
|
+
"""
|
|
701
|
+
if isinstance(element, int):
|
|
702
|
+
if element < 0 or element >= len(element_names):
|
|
703
|
+
msg = f"Element index {element} out of range"
|
|
704
|
+
raise ValueError(msg)
|
|
705
|
+
return element_names[element]
|
|
706
|
+
return element
|
|
707
|
+
|
|
708
|
+
def swap_elements(self, elem1: str | int, elem2: str | int) -> None:
|
|
709
|
+
"""Swap the positions of two elements.
|
|
710
|
+
|
|
711
|
+
Args:
|
|
712
|
+
elem1: First element to swap, specified by name or index
|
|
713
|
+
elem2: Second element to swap, specified by name or index
|
|
714
|
+
|
|
715
|
+
Example:
|
|
716
|
+
# Swap by names
|
|
717
|
+
node.swap_elements("element1", "element2")
|
|
718
|
+
|
|
719
|
+
# Swap by indices
|
|
720
|
+
node.swap_elements(0, 2)
|
|
721
|
+
|
|
722
|
+
# Mix names and indices
|
|
723
|
+
node.swap_elements("element1", 2)
|
|
724
|
+
"""
|
|
725
|
+
# Get list of all element names
|
|
726
|
+
element_names = [child.name for child in self.root_ui_element._children]
|
|
727
|
+
|
|
728
|
+
# Convert indices to names if needed
|
|
729
|
+
elem1 = self._get_element_name(elem1, element_names)
|
|
730
|
+
elem2 = self._get_element_name(elem2, element_names)
|
|
731
|
+
|
|
732
|
+
# Create new order with swapped elements
|
|
733
|
+
new_order = element_names.copy()
|
|
734
|
+
idx1 = new_order.index(elem1)
|
|
735
|
+
idx2 = new_order.index(elem2)
|
|
736
|
+
new_order[idx1], new_order[idx2] = new_order[idx2], new_order[idx1]
|
|
737
|
+
|
|
738
|
+
# Use reorder_elements to apply the swap
|
|
739
|
+
self.reorder_elements(list(new_order))
|
|
740
|
+
|
|
741
|
+
def move_element_up_down(self, element: str | int, *, up: bool = True) -> None:
|
|
742
|
+
"""Move an element up or down one position in the element list.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
element: The element to move, specified by name or index
|
|
746
|
+
up: If True, move element up one position. If False, move down one position.
|
|
747
|
+
|
|
748
|
+
Example:
|
|
749
|
+
# Move element up by name
|
|
750
|
+
node.move_element_up_down("element1", up=True)
|
|
751
|
+
|
|
752
|
+
# Move element down by index
|
|
753
|
+
node.move_element_up_down(0, up=False)
|
|
754
|
+
"""
|
|
755
|
+
# Get list of all element names
|
|
756
|
+
element_names = [child.name for child in self.root_ui_element._children]
|
|
757
|
+
|
|
758
|
+
# Convert index to name if needed
|
|
759
|
+
element = self._get_element_name(element, element_names)
|
|
760
|
+
|
|
761
|
+
# Create new order with moved element
|
|
762
|
+
new_order = element_names.copy()
|
|
763
|
+
idx = new_order.index(element)
|
|
764
|
+
|
|
765
|
+
if up:
|
|
766
|
+
if idx == 0:
|
|
767
|
+
msg = "Element is already at the top"
|
|
768
|
+
raise ValueError(msg)
|
|
769
|
+
new_order[idx], new_order[idx - 1] = new_order[idx - 1], new_order[idx]
|
|
770
|
+
else:
|
|
771
|
+
if idx == len(new_order) - 1:
|
|
772
|
+
msg = "Element is already at the bottom"
|
|
773
|
+
raise ValueError(msg)
|
|
774
|
+
new_order[idx], new_order[idx + 1] = new_order[idx + 1], new_order[idx]
|
|
775
|
+
|
|
776
|
+
# Use reorder_elements to apply the move
|
|
777
|
+
self.reorder_elements(list(new_order))
|
|
778
|
+
|
|
584
779
|
|
|
585
780
|
class ControlNode(BaseNode):
|
|
586
781
|
# Control Nodes may have one Control Input Port and at least one Control Output Port
|
|
@@ -620,6 +815,18 @@ class EndNode(BaseNode):
|
|
|
620
815
|
self.add_parameter(ControlParameterInput())
|
|
621
816
|
|
|
622
817
|
|
|
818
|
+
class StartLoopNode(BaseNode):
|
|
819
|
+
finished: bool
|
|
820
|
+
current_index: int
|
|
821
|
+
end_node: EndLoopNode | None = None
|
|
822
|
+
"""Creating class for Start Loop Node in order to implement loop functionality in execution."""
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
class EndLoopNode(BaseNode):
|
|
826
|
+
start_node: StartLoopNode | None = None
|
|
827
|
+
"""Creating class for Start Loop Node in order to implement loop functionality in execution."""
|
|
828
|
+
|
|
829
|
+
|
|
623
830
|
class Connection:
|
|
624
831
|
source_node: BaseNode
|
|
625
832
|
target_node: BaseNode
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from griptape.memory.structure import Run
|
|
4
|
+
|
|
5
|
+
from griptape_nodes.retained_mode.events.base_events import (
|
|
6
|
+
RequestPayload,
|
|
7
|
+
ResultPayloadFailure,
|
|
8
|
+
ResultPayloadSuccess,
|
|
9
|
+
WorkflowNotAlteredMixin,
|
|
10
|
+
)
|
|
11
|
+
from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
@PayloadRegistry.register
|
|
16
|
+
class RunAgentRequest(RequestPayload):
|
|
17
|
+
input: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
@PayloadRegistry.register
|
|
22
|
+
class RunAgentResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
|
|
23
|
+
output: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
@PayloadRegistry.register
|
|
28
|
+
class RunAgentResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
|
|
29
|
+
error: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
@PayloadRegistry.register
|
|
34
|
+
class GetConversationMemoryRequest(RequestPayload):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
@PayloadRegistry.register
|
|
40
|
+
class GetConversationMemoryResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
|
|
41
|
+
runs: list[Run]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
@PayloadRegistry.register
|
|
46
|
+
class GetConversationMemoryResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
@PayloadRegistry.register
|
|
52
|
+
class ConfigureAgentRequest(RequestPayload):
|
|
53
|
+
prompt_driver: dict = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
@PayloadRegistry.register
|
|
58
|
+
class ConfigureAgentResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
@PayloadRegistry.register
|
|
64
|
+
class ConfigureAgentResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
@PayloadRegistry.register
|
|
70
|
+
class ResetAgentConversationMemoryRequest(RequestPayload):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
@PayloadRegistry.register
|
|
76
|
+
class ResetAgentConversationMemoryResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
@PayloadRegistry.register
|
|
82
|
+
class ResetAgentConversationMemoryResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
|
|
83
|
+
pass
|
|
@@ -39,6 +39,7 @@ from griptape_nodes.utils.metaclasses import SingletonMeta
|
|
|
39
39
|
|
|
40
40
|
if TYPE_CHECKING:
|
|
41
41
|
from griptape_nodes.exe_types.node_types import BaseNode
|
|
42
|
+
from griptape_nodes.retained_mode.managers.agent_manager import AgentManager
|
|
42
43
|
from griptape_nodes.retained_mode.managers.arbitrary_code_exec_manager import (
|
|
43
44
|
ArbitraryCodeExecManager,
|
|
44
45
|
)
|
|
@@ -98,8 +99,10 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
98
99
|
_arbitrary_code_exec_manager: ArbitraryCodeExecManager
|
|
99
100
|
_operation_depth_manager: OperationDepthManager
|
|
100
101
|
_static_files_manager: StaticFilesManager
|
|
102
|
+
_agent_manager: AgentManager
|
|
101
103
|
|
|
102
104
|
def __init__(self) -> None:
|
|
105
|
+
from griptape_nodes.retained_mode.managers.agent_manager import AgentManager
|
|
103
106
|
from griptape_nodes.retained_mode.managers.arbitrary_code_exec_manager import (
|
|
104
107
|
ArbitraryCodeExecManager,
|
|
105
108
|
)
|
|
@@ -139,6 +142,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
139
142
|
self._static_files_manager = StaticFilesManager(
|
|
140
143
|
self._config_manager, self._secrets_manager, self._event_manager
|
|
141
144
|
)
|
|
145
|
+
self._agent_manager = AgentManager(self._event_manager)
|
|
142
146
|
|
|
143
147
|
# Assign handlers now that these are created.
|
|
144
148
|
self._event_manager.assign_manager_to_request_type(
|
|
@@ -222,6 +226,10 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
222
226
|
def StaticFilesManager(cls) -> StaticFilesManager:
|
|
223
227
|
return GriptapeNodes.get_instance()._static_files_manager
|
|
224
228
|
|
|
229
|
+
@classmethod
|
|
230
|
+
def AgentManager(cls) -> AgentManager:
|
|
231
|
+
return GriptapeNodes.get_instance()._agent_manager
|
|
232
|
+
|
|
225
233
|
@classmethod
|
|
226
234
|
def clear_data(cls) -> None:
|
|
227
235
|
# Get canvas
|