griptape-nodes 0.38.1__py3-none-any.whl → 0.41.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 +13 -9
- griptape_nodes/app/__init__.py +10 -1
- griptape_nodes/app/app.py +2 -3
- griptape_nodes/app/app_sessions.py +458 -0
- griptape_nodes/bootstrap/workflow_executors/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +213 -0
- griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +13 -0
- griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +1 -1
- griptape_nodes/drivers/storage/__init__.py +4 -0
- griptape_nodes/drivers/storage/storage_backend.py +10 -0
- griptape_nodes/exe_types/core_types.py +5 -1
- griptape_nodes/exe_types/node_types.py +20 -24
- griptape_nodes/machines/node_resolution.py +5 -1
- griptape_nodes/node_library/advanced_node_library.py +51 -0
- griptape_nodes/node_library/library_registry.py +28 -2
- griptape_nodes/node_library/workflow_registry.py +1 -1
- griptape_nodes/retained_mode/events/agent_events.py +15 -2
- griptape_nodes/retained_mode/events/app_events.py +113 -2
- griptape_nodes/retained_mode/events/base_events.py +28 -1
- griptape_nodes/retained_mode/events/library_events.py +111 -1
- griptape_nodes/retained_mode/events/node_events.py +1 -0
- griptape_nodes/retained_mode/events/workflow_events.py +1 -0
- griptape_nodes/retained_mode/griptape_nodes.py +240 -18
- griptape_nodes/retained_mode/managers/agent_manager.py +123 -17
- griptape_nodes/retained_mode/managers/flow_manager.py +16 -48
- griptape_nodes/retained_mode/managers/library_manager.py +642 -121
- griptape_nodes/retained_mode/managers/node_manager.py +2 -2
- griptape_nodes/retained_mode/managers/static_files_manager.py +4 -3
- griptape_nodes/retained_mode/managers/workflow_manager.py +666 -37
- griptape_nodes/retained_mode/utils/__init__.py +1 -0
- griptape_nodes/retained_mode/utils/engine_identity.py +131 -0
- griptape_nodes/retained_mode/utils/name_generator.py +162 -0
- griptape_nodes/retained_mode/utils/session_persistence.py +105 -0
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.41.0.dist-info}/METADATA +1 -1
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.41.0.dist-info}/RECORD +38 -28
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.41.0.dist-info}/WHEEL +0 -0
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.41.0.dist-info}/entry_points.txt +0 -0
- {griptape_nodes-0.38.1.dist-info → griptape_nodes-0.41.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -13,7 +13,8 @@ from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
|
|
|
13
13
|
@dataclass
|
|
14
14
|
@PayloadRegistry.register
|
|
15
15
|
class AppStartSessionRequest(RequestPayload):
|
|
16
|
-
|
|
16
|
+
# TODO: https://github.com/griptape-ai/griptape-nodes/issues/1600
|
|
17
|
+
session_id: str | None = None
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
@dataclass
|
|
@@ -68,5 +69,115 @@ class GetEngineVersionResultSuccess(ResultPayloadSuccess):
|
|
|
68
69
|
|
|
69
70
|
@dataclass
|
|
70
71
|
@PayloadRegistry.register
|
|
71
|
-
class GetEngineVersionResultFailure(WorkflowNotAlteredMixin,
|
|
72
|
+
class GetEngineVersionResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
@PayloadRegistry.register
|
|
78
|
+
class AppEndSessionRequest(RequestPayload):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
@PayloadRegistry.register
|
|
84
|
+
class AppEndSessionResultSuccess(ResultPayloadSuccess):
|
|
85
|
+
session_id: str | None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
@PayloadRegistry.register
|
|
90
|
+
class AppEndSessionResultFailure(ResultPayloadFailure):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
@PayloadRegistry.register
|
|
96
|
+
class SessionHeartbeatRequest(RequestPayload):
|
|
97
|
+
"""Request clients can use ensure the engine session is still active."""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
@PayloadRegistry.register
|
|
102
|
+
class SessionHeartbeatResultSuccess(ResultPayloadSuccess):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
@PayloadRegistry.register
|
|
108
|
+
class SessionHeartbeatResultFailure(ResultPayloadFailure):
|
|
72
109
|
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
@PayloadRegistry.register
|
|
114
|
+
class EngineHeartbeatRequest(RequestPayload):
|
|
115
|
+
"""Request clients can use to discover active engines and their status.
|
|
116
|
+
|
|
117
|
+
Attributes:
|
|
118
|
+
heartbeat_id: Unique identifier for the heartbeat request, used to correlate requests and responses.
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
heartbeat_id: str
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
@PayloadRegistry.register
|
|
127
|
+
class EngineHeartbeatResultSuccess(ResultPayloadSuccess):
|
|
128
|
+
heartbeat_id: str
|
|
129
|
+
engine_version: str
|
|
130
|
+
engine_id: str | None
|
|
131
|
+
session_id: str | None
|
|
132
|
+
timestamp: str
|
|
133
|
+
instance_type: str | None
|
|
134
|
+
instance_region: str | None
|
|
135
|
+
instance_provider: str | None
|
|
136
|
+
deployment_type: str | None
|
|
137
|
+
public_ip: str | None
|
|
138
|
+
current_workflow: str | None
|
|
139
|
+
workflow_file_path: str | None
|
|
140
|
+
has_active_flow: bool
|
|
141
|
+
engine_name: str
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
@PayloadRegistry.register
|
|
146
|
+
class EngineHeartbeatResultFailure(ResultPayloadFailure):
|
|
147
|
+
heartbeat_id: str
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
@PayloadRegistry.register
|
|
152
|
+
class SetEngineNameRequest(RequestPayload):
|
|
153
|
+
engine_name: str
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
@PayloadRegistry.register
|
|
158
|
+
class SetEngineNameResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
|
|
159
|
+
engine_name: str
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
@PayloadRegistry.register
|
|
164
|
+
class SetEngineNameResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
|
|
165
|
+
error_message: str
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
@PayloadRegistry.register
|
|
170
|
+
class GetEngineNameRequest(RequestPayload):
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
@PayloadRegistry.register
|
|
176
|
+
class GetEngineNameResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
|
|
177
|
+
engine_name: str
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
@PayloadRegistry.register
|
|
182
|
+
class GetEngineNameResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
|
|
183
|
+
error_message: str
|
|
@@ -12,6 +12,9 @@ from griptape.structures import Structure
|
|
|
12
12
|
from griptape.tools import BaseTool
|
|
13
13
|
from pydantic import BaseModel, Field
|
|
14
14
|
|
|
15
|
+
from griptape_nodes.retained_mode.utils.engine_identity import EngineIdentity
|
|
16
|
+
from griptape_nodes.retained_mode.utils.session_persistence import SessionPersistence
|
|
17
|
+
|
|
15
18
|
if TYPE_CHECKING:
|
|
16
19
|
import builtins
|
|
17
20
|
|
|
@@ -28,6 +31,7 @@ class RequestPayload(Payload, ABC):
|
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
# Result payload base class with abstract succeeded/failed methods, and indicator whether the current workflow was altered.
|
|
34
|
+
@dataclass(kw_only=True)
|
|
31
35
|
class ResultPayload(Payload, ABC):
|
|
32
36
|
"""Base class for all result payloads."""
|
|
33
37
|
|
|
@@ -62,6 +66,7 @@ class WorkflowNotAlteredMixin:
|
|
|
62
66
|
|
|
63
67
|
|
|
64
68
|
# Success result payload abstract base class
|
|
69
|
+
@dataclass(kw_only=True)
|
|
65
70
|
class ResultPayloadSuccess(ResultPayload, ABC):
|
|
66
71
|
"""Abstract base class for success result payloads."""
|
|
67
72
|
|
|
@@ -75,9 +80,12 @@ class ResultPayloadSuccess(ResultPayload, ABC):
|
|
|
75
80
|
|
|
76
81
|
|
|
77
82
|
# Failure result payload abstract base class
|
|
83
|
+
@dataclass(kw_only=True)
|
|
78
84
|
class ResultPayloadFailure(ResultPayload, ABC):
|
|
79
85
|
"""Abstract base class for failure result payloads."""
|
|
80
86
|
|
|
87
|
+
exception: Exception | None = None
|
|
88
|
+
|
|
81
89
|
def succeeded(self) -> bool:
|
|
82
90
|
"""Returns False as this is a failure result.
|
|
83
91
|
|
|
@@ -108,10 +116,29 @@ class BaseEvent(BaseModel, ABC):
|
|
|
108
116
|
# Keeping here instead of in GriptapeNodes to avoid circular import hell.
|
|
109
117
|
# TODO: https://github.com/griptape-ai/griptape-nodes/issues/848
|
|
110
118
|
_session_id: ClassVar[str | None] = None
|
|
119
|
+
_engine_id: ClassVar[str | None] = None
|
|
111
120
|
|
|
112
|
-
# Instance
|
|
121
|
+
# Instance variables with a default_factory that references the class variable
|
|
122
|
+
engine_id: str | None = Field(default_factory=lambda: BaseEvent._engine_id)
|
|
113
123
|
session_id: str | None = Field(default_factory=lambda: BaseEvent._session_id)
|
|
114
124
|
|
|
125
|
+
@classmethod
|
|
126
|
+
def initialize_engine_id(cls) -> None:
|
|
127
|
+
"""Initialize the engine ID if not already set."""
|
|
128
|
+
if cls._engine_id is None:
|
|
129
|
+
persisted_engine_id = cls._engine_id = EngineIdentity.get_engine_id()
|
|
130
|
+
if persisted_engine_id:
|
|
131
|
+
cls._engine_id = persisted_engine_id
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def initialize_session_id(cls) -> None:
|
|
135
|
+
"""Initialize the session ID from persisted storage if available."""
|
|
136
|
+
if cls._session_id is None:
|
|
137
|
+
# Check if there's a persisted session ID
|
|
138
|
+
persisted_session_id = SessionPersistence.get_persisted_session_id()
|
|
139
|
+
if persisted_session_id:
|
|
140
|
+
cls._session_id = persisted_session_id
|
|
141
|
+
|
|
115
142
|
# Custom JSON encoder for the payload
|
|
116
143
|
class Config:
|
|
117
144
|
"""Pydantic configuration for the BaseEvent class."""
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
2
5
|
|
|
3
|
-
from griptape_nodes.node_library.library_registry import LibraryMetadata, NodeMetadata
|
|
4
6
|
from griptape_nodes.retained_mode.events.base_events import (
|
|
5
7
|
RequestPayload,
|
|
6
8
|
ResultPayloadFailure,
|
|
@@ -10,6 +12,10 @@ from griptape_nodes.retained_mode.events.base_events import (
|
|
|
10
12
|
)
|
|
11
13
|
from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
|
|
12
14
|
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from griptape_nodes.node_library.library_registry import LibraryMetadata, LibrarySchema, NodeMetadata
|
|
17
|
+
from griptape_nodes.retained_mode.managers.library_manager import LibraryManager
|
|
18
|
+
|
|
13
19
|
|
|
14
20
|
@dataclass
|
|
15
21
|
@PayloadRegistry.register
|
|
@@ -66,6 +72,110 @@ class GetNodeMetadataFromLibraryResultFailure(WorkflowNotAlteredMixin, ResultPay
|
|
|
66
72
|
pass
|
|
67
73
|
|
|
68
74
|
|
|
75
|
+
@dataclass
|
|
76
|
+
@PayloadRegistry.register
|
|
77
|
+
class LoadLibraryMetadataFromFileRequest(RequestPayload):
|
|
78
|
+
"""Request to load library metadata from a JSON file without loading node modules.
|
|
79
|
+
|
|
80
|
+
This provides a lightweight way to get library schema information without the overhead
|
|
81
|
+
of dynamically importing Python modules. Useful for metadata queries, validation,
|
|
82
|
+
and library discovery operations.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
file_path: Absolute path to the library JSON schema file to load.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
file_path: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
@PayloadRegistry.register
|
|
93
|
+
class LoadLibraryMetadataFromFileResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
|
|
94
|
+
"""Successful result from loading library metadata.
|
|
95
|
+
|
|
96
|
+
Contains the validated library schema that can be used for metadata queries,
|
|
97
|
+
node type discovery, and other operations that don't require the actual
|
|
98
|
+
node classes to be loaded.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
library_schema: The validated LibrarySchema object containing all metadata
|
|
102
|
+
about the library including nodes, categories, and settings.
|
|
103
|
+
file_path: The file path from which the library metadata was loaded.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
library_schema: LibrarySchema
|
|
107
|
+
file_path: str
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
@PayloadRegistry.register
|
|
112
|
+
class LoadLibraryMetadataFromFileResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
|
|
113
|
+
"""Failed result from loading library metadata with detailed error information.
|
|
114
|
+
|
|
115
|
+
Provides comprehensive error details including the specific failure type and
|
|
116
|
+
a list of problems encountered during loading. This allows callers to understand
|
|
117
|
+
exactly what went wrong and take appropriate action.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
library_path: Path to the library file that failed to load.
|
|
121
|
+
library_name: Name of the library if it could be extracted from the JSON,
|
|
122
|
+
None if the name couldn't be determined.
|
|
123
|
+
status: The LibraryStatus enum indicating the type of failure
|
|
124
|
+
(MISSING, UNUSABLE, etc.).
|
|
125
|
+
problems: List of specific error messages describing what went wrong
|
|
126
|
+
during loading (JSON parse errors, validation failures, etc.).
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
library_path: str
|
|
130
|
+
library_name: str | None
|
|
131
|
+
status: LibraryManager.LibraryStatus
|
|
132
|
+
problems: list[str]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
@PayloadRegistry.register
|
|
137
|
+
class LoadMetadataForAllLibrariesRequest(RequestPayload):
|
|
138
|
+
"""Request to load metadata for all libraries from configuration without loading node modules.
|
|
139
|
+
|
|
140
|
+
This loads metadata from both:
|
|
141
|
+
1. Library JSON files specified in configuration
|
|
142
|
+
2. Sandbox library (dynamically generated from Python files)
|
|
143
|
+
|
|
144
|
+
Provides a lightweight way to discover all available libraries and their schemas
|
|
145
|
+
without the overhead of importing Python modules or registering them in the system.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
@PayloadRegistry.register
|
|
151
|
+
class LoadMetadataForAllLibrariesResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
|
|
152
|
+
"""Successful result from loading metadata for all libraries.
|
|
153
|
+
|
|
154
|
+
Contains metadata for all discoverable libraries from both configuration files
|
|
155
|
+
and sandbox directory, with clear separation between successful loads and failures.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
successful_libraries: List of successful library metadata loading results,
|
|
159
|
+
including both config-based libraries and sandbox library if applicable.
|
|
160
|
+
failed_libraries: List of detailed failure results for libraries that couldn't be loaded,
|
|
161
|
+
including both config-based libraries and sandbox library if applicable.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
successful_libraries: list[LoadLibraryMetadataFromFileResultSuccess]
|
|
165
|
+
failed_libraries: list[LoadLibraryMetadataFromFileResultFailure]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
@PayloadRegistry.register
|
|
170
|
+
class LoadMetadataForAllLibrariesResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
|
|
171
|
+
"""Failed result from loading metadata for all libraries.
|
|
172
|
+
|
|
173
|
+
This indicates a systemic failure (e.g., configuration access issues)
|
|
174
|
+
rather than individual library loading failures, which are captured
|
|
175
|
+
in the success result's failed_libraries list.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
|
|
69
179
|
@dataclass
|
|
70
180
|
@PayloadRegistry.register
|
|
71
181
|
class RegisterLibraryFromFileRequest(RequestPayload):
|
|
@@ -408,6 +408,7 @@ class DeserializeNodeFromCommandsResultFailure(ResultPayloadFailure):
|
|
|
408
408
|
@PayloadRegistry.register
|
|
409
409
|
class DuplicateSelectedNodesRequest(WorkflowNotAlteredMixin, RequestPayload):
|
|
410
410
|
nodes_to_duplicate: list[list[str]]
|
|
411
|
+
positions: list[NewPosition] | None = None
|
|
411
412
|
|
|
412
413
|
|
|
413
414
|
@dataclass
|
|
@@ -2,27 +2,48 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import importlib.metadata
|
|
4
4
|
import logging
|
|
5
|
+
import os
|
|
5
6
|
import re
|
|
7
|
+
import uuid
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
from datetime import UTC, datetime
|
|
8
10
|
from typing import IO, TYPE_CHECKING, Any, TextIO
|
|
9
11
|
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
10
14
|
from griptape_nodes.exe_types.core_types import BaseNodeElement, Parameter, ParameterContainer, ParameterGroup
|
|
11
15
|
from griptape_nodes.exe_types.flow import ControlFlow
|
|
16
|
+
from griptape_nodes.node_library.workflow_registry import WorkflowRegistry
|
|
12
17
|
from griptape_nodes.retained_mode.events.app_events import (
|
|
18
|
+
AppEndSessionRequest,
|
|
19
|
+
AppEndSessionResultFailure,
|
|
20
|
+
AppEndSessionResultSuccess,
|
|
13
21
|
AppGetSessionRequest,
|
|
14
22
|
AppGetSessionResultSuccess,
|
|
15
23
|
AppStartSessionRequest,
|
|
16
24
|
AppStartSessionResultSuccess,
|
|
25
|
+
EngineHeartbeatRequest,
|
|
26
|
+
EngineHeartbeatResultFailure,
|
|
27
|
+
EngineHeartbeatResultSuccess,
|
|
28
|
+
GetEngineNameRequest,
|
|
29
|
+
GetEngineNameResultFailure,
|
|
30
|
+
GetEngineNameResultSuccess,
|
|
17
31
|
GetEngineVersionRequest,
|
|
18
32
|
GetEngineVersionResultFailure,
|
|
19
33
|
GetEngineVersionResultSuccess,
|
|
34
|
+
SessionHeartbeatRequest,
|
|
35
|
+
SessionHeartbeatResultFailure,
|
|
36
|
+
SessionHeartbeatResultSuccess,
|
|
37
|
+
SetEngineNameRequest,
|
|
38
|
+
SetEngineNameResultFailure,
|
|
39
|
+
SetEngineNameResultSuccess,
|
|
20
40
|
)
|
|
21
41
|
from griptape_nodes.retained_mode.events.base_events import (
|
|
22
42
|
AppPayload,
|
|
23
43
|
BaseEvent,
|
|
24
44
|
RequestPayload,
|
|
25
45
|
ResultPayload,
|
|
46
|
+
ResultPayloadFailure,
|
|
26
47
|
)
|
|
27
48
|
from griptape_nodes.retained_mode.events.connection_events import (
|
|
28
49
|
CreateConnectionRequest,
|
|
@@ -35,6 +56,8 @@ from griptape_nodes.retained_mode.events.parameter_events import (
|
|
|
35
56
|
AddParameterToNodeRequest,
|
|
36
57
|
AlterParameterDetailsRequest,
|
|
37
58
|
)
|
|
59
|
+
from griptape_nodes.retained_mode.utils.engine_identity import EngineIdentity
|
|
60
|
+
from griptape_nodes.retained_mode.utils.session_persistence import SessionPersistence
|
|
38
61
|
from griptape_nodes.utils.metaclasses import SingletonMeta
|
|
39
62
|
|
|
40
63
|
if TYPE_CHECKING:
|
|
@@ -169,7 +192,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
169
192
|
self._static_files_manager = StaticFilesManager(
|
|
170
193
|
self._config_manager, self._secrets_manager, self._event_manager
|
|
171
194
|
)
|
|
172
|
-
self._agent_manager = AgentManager(self._event_manager)
|
|
195
|
+
self._agent_manager = AgentManager(self._static_files_manager, self._event_manager)
|
|
173
196
|
self._version_compatibility_manager = VersionCompatibilityManager(self._event_manager)
|
|
174
197
|
|
|
175
198
|
# Assign handlers now that these are created.
|
|
@@ -179,7 +202,20 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
179
202
|
self._event_manager.assign_manager_to_request_type(
|
|
180
203
|
AppStartSessionRequest, self.handle_session_start_request
|
|
181
204
|
)
|
|
205
|
+
self._event_manager.assign_manager_to_request_type(AppEndSessionRequest, self.handle_session_end_request)
|
|
182
206
|
self._event_manager.assign_manager_to_request_type(AppGetSessionRequest, self.handle_get_session_request)
|
|
207
|
+
self._event_manager.assign_manager_to_request_type(
|
|
208
|
+
SessionHeartbeatRequest, self.handle_session_heartbeat_request
|
|
209
|
+
)
|
|
210
|
+
self._event_manager.assign_manager_to_request_type(
|
|
211
|
+
EngineHeartbeatRequest, self.handle_engine_heartbeat_request
|
|
212
|
+
)
|
|
213
|
+
self._event_manager.assign_manager_to_request_type(
|
|
214
|
+
GetEngineNameRequest, self.handle_get_engine_name_request
|
|
215
|
+
)
|
|
216
|
+
self._event_manager.assign_manager_to_request_type(
|
|
217
|
+
SetEngineNameRequest, self.handle_set_engine_name_request
|
|
218
|
+
)
|
|
183
219
|
|
|
184
220
|
@classmethod
|
|
185
221
|
def get_instance(cls) -> GriptapeNodes:
|
|
@@ -191,11 +227,20 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
191
227
|
event_mgr = GriptapeNodes.EventManager()
|
|
192
228
|
obj_depth_mgr = GriptapeNodes.OperationDepthManager()
|
|
193
229
|
workflow_mgr = GriptapeNodes.WorkflowManager()
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
return event_mgr.handle_request(
|
|
233
|
+
request=request,
|
|
234
|
+
operation_depth_mgr=obj_depth_mgr,
|
|
235
|
+
workflow_mgr=workflow_mgr,
|
|
236
|
+
)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.exception(
|
|
239
|
+
"Unhandled exception while processing request of type %s. "
|
|
240
|
+
"Consider saving your work and restarting the engine if issues persist.",
|
|
241
|
+
type(request).__name__,
|
|
242
|
+
)
|
|
243
|
+
return ResultPayloadFailure(exception=e)
|
|
199
244
|
|
|
200
245
|
@classmethod
|
|
201
246
|
def broadcast_app_event(cls, app_event: AppPayload) -> None:
|
|
@@ -302,26 +347,203 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
302
347
|
logger.error(details)
|
|
303
348
|
return GetEngineVersionResultFailure()
|
|
304
349
|
|
|
305
|
-
def handle_session_start_request(self, request: AppStartSessionRequest) -> ResultPayload:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
350
|
+
def handle_session_start_request(self, request: AppStartSessionRequest) -> ResultPayload: # noqa: ARG002
|
|
351
|
+
current_session_id = BaseEvent._session_id
|
|
352
|
+
if current_session_id is None:
|
|
353
|
+
# Client wants a new session
|
|
354
|
+
current_session_id = uuid.uuid4().hex
|
|
355
|
+
BaseEvent._session_id = current_session_id
|
|
356
|
+
# Persist the session ID to XDG state directory
|
|
357
|
+
SessionPersistence.persist_session(current_session_id)
|
|
358
|
+
details = f"New session '{current_session_id}' started at {datetime.now(tz=UTC)}."
|
|
314
359
|
logger.info(details)
|
|
360
|
+
else:
|
|
361
|
+
details = f"Session '{current_session_id}' already active. Joining..."
|
|
315
362
|
|
|
316
|
-
|
|
363
|
+
return AppStartSessionResultSuccess(current_session_id)
|
|
317
364
|
|
|
318
|
-
|
|
365
|
+
def handle_session_end_request(self, _: AppEndSessionRequest) -> ResultPayload:
|
|
366
|
+
try:
|
|
367
|
+
previous_session_id = BaseEvent._session_id
|
|
368
|
+
if BaseEvent._session_id is None:
|
|
369
|
+
details = "No active session to end."
|
|
370
|
+
logger.info(details)
|
|
371
|
+
else:
|
|
372
|
+
details = f"Session '{BaseEvent._session_id}' ended at {datetime.now(tz=UTC)}."
|
|
373
|
+
logger.info(details)
|
|
374
|
+
BaseEvent._session_id = None
|
|
375
|
+
# Clear the persisted session ID from XDG state directory
|
|
376
|
+
SessionPersistence.clear_persisted_session()
|
|
319
377
|
|
|
320
|
-
|
|
378
|
+
return AppEndSessionResultSuccess(session_id=previous_session_id)
|
|
379
|
+
except Exception as err:
|
|
380
|
+
details = f"Failed to end session due to '{err}'."
|
|
381
|
+
logger.error(details)
|
|
382
|
+
return AppEndSessionResultFailure()
|
|
321
383
|
|
|
322
384
|
def handle_get_session_request(self, _: AppGetSessionRequest) -> ResultPayload:
|
|
323
385
|
return AppGetSessionResultSuccess(session_id=BaseEvent._session_id)
|
|
324
386
|
|
|
387
|
+
def handle_session_heartbeat_request(self, request: SessionHeartbeatRequest) -> ResultPayload: # noqa: ARG002
|
|
388
|
+
"""Handle session heartbeat requests.
|
|
389
|
+
|
|
390
|
+
Simply verifies that the session is active and responds with success.
|
|
391
|
+
"""
|
|
392
|
+
try:
|
|
393
|
+
if BaseEvent._session_id is None:
|
|
394
|
+
logger.warning("Session heartbeat received but no active session found")
|
|
395
|
+
return SessionHeartbeatResultFailure()
|
|
396
|
+
|
|
397
|
+
logger.debug("Session heartbeat successful for session: %s", BaseEvent._session_id)
|
|
398
|
+
return SessionHeartbeatResultSuccess()
|
|
399
|
+
except Exception as err:
|
|
400
|
+
logger.error("Failed to handle session heartbeat: %s", err)
|
|
401
|
+
return SessionHeartbeatResultFailure()
|
|
402
|
+
|
|
403
|
+
def handle_engine_heartbeat_request(self, request: EngineHeartbeatRequest) -> ResultPayload:
|
|
404
|
+
"""Handle engine heartbeat requests.
|
|
405
|
+
|
|
406
|
+
Returns engine status information including version, session state, and system metrics.
|
|
407
|
+
"""
|
|
408
|
+
try:
|
|
409
|
+
# Get instance information based on environment variables
|
|
410
|
+
instance_info = self._get_instance_info()
|
|
411
|
+
|
|
412
|
+
# Get current workflow information
|
|
413
|
+
workflow_info = self._get_current_workflow_info()
|
|
414
|
+
|
|
415
|
+
# Get engine name
|
|
416
|
+
engine_name = EngineIdentity.get_engine_name()
|
|
417
|
+
|
|
418
|
+
logger.debug("Engine heartbeat successful")
|
|
419
|
+
return EngineHeartbeatResultSuccess(
|
|
420
|
+
heartbeat_id=request.heartbeat_id,
|
|
421
|
+
engine_version=engine_version,
|
|
422
|
+
engine_name=engine_name,
|
|
423
|
+
engine_id=BaseEvent._engine_id,
|
|
424
|
+
session_id=BaseEvent._session_id,
|
|
425
|
+
timestamp=datetime.now(tz=UTC).isoformat(),
|
|
426
|
+
**instance_info,
|
|
427
|
+
**workflow_info,
|
|
428
|
+
)
|
|
429
|
+
except Exception as err:
|
|
430
|
+
logger.error("Failed to handle engine heartbeat: %s", err)
|
|
431
|
+
return EngineHeartbeatResultFailure(heartbeat_id=request.heartbeat_id)
|
|
432
|
+
|
|
433
|
+
def handle_get_engine_name_request(self, request: GetEngineNameRequest) -> ResultPayload: # noqa: ARG002
|
|
434
|
+
"""Handle requests to get the current engine name."""
|
|
435
|
+
try:
|
|
436
|
+
engine_name = EngineIdentity.get_engine_name()
|
|
437
|
+
logger.debug("Retrieved engine name: %s", engine_name)
|
|
438
|
+
return GetEngineNameResultSuccess(engine_name=engine_name)
|
|
439
|
+
except Exception as err:
|
|
440
|
+
error_message = f"Failed to get engine name: {err}"
|
|
441
|
+
logger.error(error_message)
|
|
442
|
+
return GetEngineNameResultFailure(error_message=error_message)
|
|
443
|
+
|
|
444
|
+
def handle_set_engine_name_request(self, request: SetEngineNameRequest) -> ResultPayload:
|
|
445
|
+
"""Handle requests to set a new engine name."""
|
|
446
|
+
try:
|
|
447
|
+
# Validate engine name (basic validation)
|
|
448
|
+
if not request.engine_name or not request.engine_name.strip():
|
|
449
|
+
error_message = "Engine name cannot be empty"
|
|
450
|
+
logger.warning(error_message)
|
|
451
|
+
return SetEngineNameResultFailure(error_message=error_message)
|
|
452
|
+
|
|
453
|
+
# Set the new engine name
|
|
454
|
+
EngineIdentity.set_engine_name(request.engine_name.strip())
|
|
455
|
+
logger.info("Engine name set to: %s", request.engine_name.strip())
|
|
456
|
+
return SetEngineNameResultSuccess(engine_name=request.engine_name.strip())
|
|
457
|
+
|
|
458
|
+
except Exception as err:
|
|
459
|
+
error_message = f"Failed to set engine name: {err}"
|
|
460
|
+
logger.error(error_message)
|
|
461
|
+
return SetEngineNameResultFailure(error_message=error_message)
|
|
462
|
+
|
|
463
|
+
def _get_instance_info(self) -> dict[str, str | None]:
|
|
464
|
+
"""Get instance information from environment variables.
|
|
465
|
+
|
|
466
|
+
Returns instance type, region, provider, and public IP information if available.
|
|
467
|
+
"""
|
|
468
|
+
instance_info: dict[str, str | None] = {
|
|
469
|
+
"instance_type": os.getenv("GTN_INSTANCE_TYPE"),
|
|
470
|
+
"instance_region": os.getenv("GTN_INSTANCE_REGION"),
|
|
471
|
+
"instance_provider": os.getenv("GTN_INSTANCE_PROVIDER"),
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
# Determine deployment type based on presence of instance environment variables
|
|
475
|
+
instance_info["deployment_type"] = "griptape_hosted" if any(instance_info.values()) else "local"
|
|
476
|
+
|
|
477
|
+
# Get public IP address
|
|
478
|
+
public_ip = self._get_public_ip()
|
|
479
|
+
if public_ip:
|
|
480
|
+
instance_info["public_ip"] = public_ip
|
|
481
|
+
|
|
482
|
+
return instance_info
|
|
483
|
+
|
|
484
|
+
def _get_public_ip(self) -> str | None:
|
|
485
|
+
"""Get the public IP address of this device.
|
|
486
|
+
|
|
487
|
+
Returns the public IP address if available, None otherwise.
|
|
488
|
+
"""
|
|
489
|
+
try:
|
|
490
|
+
# Try multiple services in case one is down
|
|
491
|
+
services = [
|
|
492
|
+
"https://api.ipify.org",
|
|
493
|
+
"https://ipinfo.io/ip",
|
|
494
|
+
"https://icanhazip.com",
|
|
495
|
+
]
|
|
496
|
+
|
|
497
|
+
for service in services:
|
|
498
|
+
try:
|
|
499
|
+
with httpx.Client(timeout=5.0) as client:
|
|
500
|
+
response = client.get(service)
|
|
501
|
+
response.raise_for_status()
|
|
502
|
+
public_ip = response.text.strip()
|
|
503
|
+
if public_ip:
|
|
504
|
+
logger.debug("Retrieved public IP from %s: %s", service, public_ip)
|
|
505
|
+
return public_ip
|
|
506
|
+
except Exception as err:
|
|
507
|
+
logger.debug("Failed to get public IP from %s: %s", service, err)
|
|
508
|
+
continue
|
|
509
|
+
logger.warning("Unable to retrieve public IP from any service")
|
|
510
|
+
except Exception as err:
|
|
511
|
+
logger.warning("Failed to get public IP: %s", err)
|
|
512
|
+
return None
|
|
513
|
+
else:
|
|
514
|
+
return None
|
|
515
|
+
|
|
516
|
+
def _get_current_workflow_info(self) -> dict[str, Any]:
|
|
517
|
+
"""Get information about the currently loaded workflow.
|
|
518
|
+
|
|
519
|
+
Returns workflow name, file path, and status information if available.
|
|
520
|
+
"""
|
|
521
|
+
workflow_info = {
|
|
522
|
+
"current_workflow": None,
|
|
523
|
+
"workflow_file_path": None,
|
|
524
|
+
"has_active_flow": False,
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
try:
|
|
528
|
+
context_manager = self._context_manager
|
|
529
|
+
|
|
530
|
+
# Check if there's an active workflow
|
|
531
|
+
if context_manager.has_current_workflow():
|
|
532
|
+
workflow_name = context_manager.get_current_workflow_name()
|
|
533
|
+
workflow_info["current_workflow"] = workflow_name
|
|
534
|
+
workflow_info["has_active_flow"] = context_manager.has_current_flow()
|
|
535
|
+
|
|
536
|
+
# Get workflow file path from registry
|
|
537
|
+
if WorkflowRegistry.has_workflow_with_name(workflow_name):
|
|
538
|
+
workflow = WorkflowRegistry.get_workflow_by_name(workflow_name)
|
|
539
|
+
absolute_path = WorkflowRegistry.get_complete_file_path(workflow.file_path)
|
|
540
|
+
workflow_info["workflow_file_path"] = absolute_path
|
|
541
|
+
|
|
542
|
+
except Exception as err:
|
|
543
|
+
logger.warning("Failed to get current workflow info: %s", err)
|
|
544
|
+
|
|
545
|
+
return workflow_info
|
|
546
|
+
|
|
325
547
|
|
|
326
548
|
def create_flows_in_order(flow_name: str, flow_manager: FlowManager, created_flows: list, file: IO) -> list | None:
|
|
327
549
|
"""Creates flows in the correct order based on their dependencies."""
|