griptape-nodes 0.41.0__py3-none-any.whl → 0.43.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 +0 -0
- griptape_nodes/app/.python-version +0 -0
- griptape_nodes/app/__init__.py +1 -10
- griptape_nodes/app/api.py +199 -0
- griptape_nodes/app/app.py +140 -222
- griptape_nodes/app/watch.py +4 -2
- griptape_nodes/bootstrap/__init__.py +0 -0
- griptape_nodes/bootstrap/bootstrap_script.py +0 -0
- griptape_nodes/bootstrap/register_libraries_script.py +0 -0
- griptape_nodes/bootstrap/structure_config.yaml +0 -0
- griptape_nodes/bootstrap/workflow_executors/__init__.py +0 -0
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +0 -0
- griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +0 -0
- griptape_nodes/bootstrap/workflow_runners/__init__.py +0 -0
- griptape_nodes/bootstrap/workflow_runners/bootstrap_workflow_runner.py +0 -0
- griptape_nodes/bootstrap/workflow_runners/local_workflow_runner.py +0 -0
- griptape_nodes/bootstrap/workflow_runners/subprocess_workflow_runner.py +6 -2
- griptape_nodes/bootstrap/workflow_runners/workflow_runner.py +0 -0
- griptape_nodes/drivers/__init__.py +0 -0
- griptape_nodes/drivers/storage/__init__.py +0 -0
- griptape_nodes/drivers/storage/base_storage_driver.py +0 -0
- griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +0 -0
- griptape_nodes/drivers/storage/local_storage_driver.py +5 -3
- griptape_nodes/drivers/storage/storage_backend.py +0 -0
- griptape_nodes/exe_types/__init__.py +0 -0
- griptape_nodes/exe_types/connections.py +0 -0
- griptape_nodes/exe_types/core_types.py +0 -0
- griptape_nodes/exe_types/flow.py +68 -368
- griptape_nodes/exe_types/node_types.py +17 -1
- griptape_nodes/exe_types/type_validator.py +0 -0
- griptape_nodes/machines/__init__.py +0 -0
- griptape_nodes/machines/control_flow.py +52 -20
- griptape_nodes/machines/fsm.py +16 -2
- griptape_nodes/machines/node_resolution.py +16 -14
- griptape_nodes/mcp_server/__init__.py +1 -0
- griptape_nodes/mcp_server/server.py +126 -0
- griptape_nodes/mcp_server/ws_request_manager.py +268 -0
- griptape_nodes/node_library/__init__.py +0 -0
- griptape_nodes/node_library/advanced_node_library.py +0 -0
- griptape_nodes/node_library/library_registry.py +0 -0
- griptape_nodes/node_library/workflow_registry.py +2 -2
- griptape_nodes/py.typed +0 -0
- griptape_nodes/retained_mode/__init__.py +0 -0
- griptape_nodes/retained_mode/events/__init__.py +0 -0
- griptape_nodes/retained_mode/events/agent_events.py +70 -8
- griptape_nodes/retained_mode/events/app_events.py +137 -12
- griptape_nodes/retained_mode/events/arbitrary_python_events.py +23 -0
- griptape_nodes/retained_mode/events/base_events.py +13 -31
- griptape_nodes/retained_mode/events/config_events.py +87 -11
- griptape_nodes/retained_mode/events/connection_events.py +56 -5
- griptape_nodes/retained_mode/events/context_events.py +27 -4
- griptape_nodes/retained_mode/events/execution_events.py +99 -14
- griptape_nodes/retained_mode/events/flow_events.py +165 -7
- griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +0 -0
- griptape_nodes/retained_mode/events/library_events.py +195 -17
- griptape_nodes/retained_mode/events/logger_events.py +11 -0
- griptape_nodes/retained_mode/events/node_events.py +242 -22
- griptape_nodes/retained_mode/events/object_events.py +40 -4
- griptape_nodes/retained_mode/events/os_events.py +116 -3
- griptape_nodes/retained_mode/events/parameter_events.py +212 -8
- griptape_nodes/retained_mode/events/payload_registry.py +0 -0
- griptape_nodes/retained_mode/events/secrets_events.py +59 -7
- griptape_nodes/retained_mode/events/static_file_events.py +57 -4
- griptape_nodes/retained_mode/events/validation_events.py +39 -4
- griptape_nodes/retained_mode/events/workflow_events.py +188 -17
- griptape_nodes/retained_mode/griptape_nodes.py +89 -363
- griptape_nodes/retained_mode/managers/__init__.py +0 -0
- griptape_nodes/retained_mode/managers/agent_manager.py +49 -23
- griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +0 -0
- griptape_nodes/retained_mode/managers/config_manager.py +0 -0
- griptape_nodes/retained_mode/managers/context_manager.py +0 -0
- griptape_nodes/retained_mode/managers/engine_identity_manager.py +146 -0
- griptape_nodes/retained_mode/managers/event_manager.py +14 -2
- griptape_nodes/retained_mode/managers/flow_manager.py +751 -64
- griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +45 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +191 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +346 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +439 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +17 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +82 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +116 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +352 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +104 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +155 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +18 -0
- griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +12 -0
- griptape_nodes/retained_mode/managers/library_manager.py +255 -40
- griptape_nodes/retained_mode/managers/node_manager.py +120 -103
- griptape_nodes/retained_mode/managers/object_manager.py +11 -3
- griptape_nodes/retained_mode/managers/operation_manager.py +0 -0
- griptape_nodes/retained_mode/managers/os_manager.py +582 -8
- griptape_nodes/retained_mode/managers/secrets_manager.py +4 -0
- griptape_nodes/retained_mode/managers/session_manager.py +328 -0
- griptape_nodes/retained_mode/managers/settings.py +7 -0
- griptape_nodes/retained_mode/managers/static_files_manager.py +0 -0
- griptape_nodes/retained_mode/managers/version_compatibility_manager.py +2 -2
- griptape_nodes/retained_mode/managers/workflow_manager.py +722 -456
- griptape_nodes/retained_mode/retained_mode.py +44 -0
- griptape_nodes/retained_mode/utils/__init__.py +0 -0
- griptape_nodes/retained_mode/utils/engine_identity.py +141 -27
- griptape_nodes/retained_mode/utils/name_generator.py +0 -0
- griptape_nodes/traits/__init__.py +0 -0
- griptape_nodes/traits/add_param_button.py +0 -0
- griptape_nodes/traits/button.py +0 -0
- griptape_nodes/traits/clamp.py +0 -0
- griptape_nodes/traits/compare.py +0 -0
- griptape_nodes/traits/compare_images.py +0 -0
- griptape_nodes/traits/file_system_picker.py +127 -0
- griptape_nodes/traits/minmax.py +0 -0
- griptape_nodes/traits/options.py +0 -0
- griptape_nodes/traits/slider.py +0 -0
- griptape_nodes/traits/trait_registry.py +0 -0
- griptape_nodes/traits/traits.json +0 -0
- griptape_nodes/updater/__init__.py +2 -2
- griptape_nodes/updater/__main__.py +0 -0
- griptape_nodes/utils/__init__.py +0 -0
- griptape_nodes/utils/dict_utils.py +0 -0
- griptape_nodes/utils/image_preview.py +128 -0
- griptape_nodes/utils/metaclasses.py +0 -0
- griptape_nodes/version_compatibility/__init__.py +0 -0
- griptape_nodes/version_compatibility/versions/__init__.py +0 -0
- griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py +0 -0
- griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +5 -5
- griptape_nodes-0.43.0.dist-info/METADATA +90 -0
- griptape_nodes-0.43.0.dist-info/RECORD +129 -0
- griptape_nodes-0.43.0.dist-info/WHEEL +4 -0
- {griptape_nodes-0.41.0.dist-info → griptape_nodes-0.43.0.dist-info}/entry_points.txt +1 -0
- griptape_nodes/app/app_sessions.py +0 -458
- griptape_nodes/retained_mode/utils/session_persistence.py +0 -105
- griptape_nodes-0.41.0.dist-info/METADATA +0 -78
- griptape_nodes-0.41.0.dist-info/RECORD +0 -112
- griptape_nodes-0.41.0.dist-info/WHEEL +0 -4
- griptape_nodes-0.41.0.dist-info/licenses/LICENSE +0 -201
griptape_nodes/machines/fsm.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any,
|
|
1
|
+
from typing import Any, TypeVar
|
|
2
2
|
|
|
3
3
|
T = TypeVar("T")
|
|
4
4
|
|
|
@@ -19,8 +19,13 @@ class State:
|
|
|
19
19
|
"""Called when exiting the state."""
|
|
20
20
|
return
|
|
21
21
|
|
|
22
|
+
@staticmethod
|
|
23
|
+
def on_event(context: Any, event: Any) -> type["State"] | None: # noqa: ARG004
|
|
24
|
+
"""Called on an event, which may trigger a State transition."""
|
|
25
|
+
return None
|
|
26
|
+
|
|
22
27
|
|
|
23
|
-
class FSM
|
|
28
|
+
class FSM[T]:
|
|
24
29
|
def __init__(self, context: T) -> None:
|
|
25
30
|
self._context = context
|
|
26
31
|
self._current_state = None
|
|
@@ -53,3 +58,12 @@ class FSM(Generic[T]):
|
|
|
53
58
|
|
|
54
59
|
if new_state is not None:
|
|
55
60
|
self.transition_state(new_state)
|
|
61
|
+
|
|
62
|
+
def handle_event(self, event: Any) -> None:
|
|
63
|
+
if self._current_state is None:
|
|
64
|
+
new_state = None
|
|
65
|
+
else:
|
|
66
|
+
new_state = self._current_state.on_event(self._context, event)
|
|
67
|
+
|
|
68
|
+
if new_state is not None:
|
|
69
|
+
self.transition_state(new_state)
|
|
@@ -4,7 +4,7 @@ import logging
|
|
|
4
4
|
from collections.abc import Generator
|
|
5
5
|
from concurrent.futures import Future, ThreadPoolExecutor
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import Any
|
|
8
8
|
|
|
9
9
|
from griptape.events import EventBus
|
|
10
10
|
from griptape.utils import with_contextvars
|
|
@@ -31,10 +31,6 @@ from griptape_nodes.retained_mode.events.parameter_events import (
|
|
|
31
31
|
SetParameterValueRequest,
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
if TYPE_CHECKING:
|
|
35
|
-
from griptape_nodes.exe_types.flow import ControlFlow
|
|
36
|
-
|
|
37
|
-
|
|
38
34
|
logger = logging.getLogger("griptape_nodes")
|
|
39
35
|
|
|
40
36
|
|
|
@@ -47,12 +43,10 @@ class Focus:
|
|
|
47
43
|
|
|
48
44
|
# This is on a per-node basis
|
|
49
45
|
class ResolutionContext:
|
|
50
|
-
flow: ControlFlow
|
|
51
46
|
focus_stack: list[Focus]
|
|
52
47
|
paused: bool
|
|
53
48
|
|
|
54
|
-
def __init__(self
|
|
55
|
-
self.flow = flow
|
|
49
|
+
def __init__(self) -> None:
|
|
56
50
|
self.focus_stack = []
|
|
57
51
|
self.paused = False
|
|
58
52
|
|
|
@@ -90,7 +84,9 @@ class InitializeSpotlightState(State):
|
|
|
90
84
|
if current_node.state == NodeResolutionState.UNRESOLVED:
|
|
91
85
|
# Mark all future nodes unresolved.
|
|
92
86
|
# TODO: https://github.com/griptape-ai/griptape-nodes/issues/862
|
|
93
|
-
|
|
87
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
88
|
+
|
|
89
|
+
GriptapeNodes.FlowManager().get_connections().unresolve_future_nodes(current_node)
|
|
94
90
|
current_node.initialize_spotlight()
|
|
95
91
|
# Set node to resolving - we are now resolving this node.
|
|
96
92
|
current_node.state = NodeResolutionState.RESOLVING
|
|
@@ -132,7 +128,9 @@ class EvaluateParameterState(State):
|
|
|
132
128
|
def on_update(context: ResolutionContext) -> type[State] | None:
|
|
133
129
|
current_node = context.focus_stack[-1].node
|
|
134
130
|
current_parameter = current_node.get_current_parameter()
|
|
135
|
-
|
|
131
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
132
|
+
|
|
133
|
+
connections = GriptapeNodes.FlowManager().get_connections()
|
|
136
134
|
if current_parameter is None:
|
|
137
135
|
msg = "No current parameter set."
|
|
138
136
|
raise ValueError(msg)
|
|
@@ -270,7 +268,10 @@ class ExecuteNodeState(State):
|
|
|
270
268
|
)
|
|
271
269
|
current_focus.process_generator = None
|
|
272
270
|
current_focus.scheduled_value = None
|
|
273
|
-
|
|
271
|
+
|
|
272
|
+
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
273
|
+
|
|
274
|
+
GriptapeNodes.FlowManager().cancel_flow_run()
|
|
274
275
|
|
|
275
276
|
EventBus.publish_event(
|
|
276
277
|
ExecutionGriptapeNodeEvent(
|
|
@@ -320,7 +321,7 @@ class ExecuteNodeState(State):
|
|
|
320
321
|
)
|
|
321
322
|
)
|
|
322
323
|
# Pass the value through to the new nodes.
|
|
323
|
-
conn_output_nodes =
|
|
324
|
+
conn_output_nodes = GriptapeNodes.FlowManager().get_connected_output_parameters(current_node, parameter)
|
|
324
325
|
for target_node, target_parameter in conn_output_nodes:
|
|
325
326
|
GriptapeNodes.get_instance().handle_request(
|
|
326
327
|
SetParameterValueRequest(
|
|
@@ -416,6 +417,7 @@ class ExecuteNodeState(State):
|
|
|
416
417
|
|
|
417
418
|
# Once we've passed on the scheduled value, we should clear it out just in case
|
|
418
419
|
current_focus.scheduled_value = None
|
|
420
|
+
|
|
419
421
|
future = ExecuteNodeState.executor.submit(with_contextvars(func))
|
|
420
422
|
future.add_done_callback(with_contextvars(on_future_done))
|
|
421
423
|
except StopIteration:
|
|
@@ -445,8 +447,8 @@ class CompleteState(State):
|
|
|
445
447
|
class NodeResolutionMachine(FSM[ResolutionContext]):
|
|
446
448
|
"""State machine for resolving node dependencies."""
|
|
447
449
|
|
|
448
|
-
def __init__(self
|
|
449
|
-
resolution_context = ResolutionContext(
|
|
450
|
+
def __init__(self) -> None:
|
|
451
|
+
resolution_context = ResolutionContext()
|
|
450
452
|
super().__init__(resolution_context)
|
|
451
453
|
|
|
452
454
|
def resolve_node(self, node: BaseNode) -> None:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Runs the Griptape Nodes MCP server."""
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
|
|
7
|
+
import uvicorn
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from mcp.server.lowlevel import Server
|
|
10
|
+
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
11
|
+
from mcp.types import (
|
|
12
|
+
TextContent,
|
|
13
|
+
Tool,
|
|
14
|
+
)
|
|
15
|
+
from pydantic import TypeAdapter
|
|
16
|
+
from rich.logging import RichHandler
|
|
17
|
+
from starlette.types import Receive, Scope, Send
|
|
18
|
+
|
|
19
|
+
from griptape_nodes.mcp_server.ws_request_manager import AsyncRequestManager, WebSocketConnectionManager
|
|
20
|
+
from griptape_nodes.retained_mode.events.base_events import RequestPayload
|
|
21
|
+
from griptape_nodes.retained_mode.events.connection_events import (
|
|
22
|
+
CreateConnectionRequest,
|
|
23
|
+
DeleteConnectionRequest,
|
|
24
|
+
ListConnectionsForNodeRequest,
|
|
25
|
+
)
|
|
26
|
+
from griptape_nodes.retained_mode.events.flow_events import ListNodesInFlowRequest
|
|
27
|
+
from griptape_nodes.retained_mode.events.node_events import (
|
|
28
|
+
CreateNodeRequest,
|
|
29
|
+
DeleteNodeRequest,
|
|
30
|
+
GetNodeMetadataRequest,
|
|
31
|
+
GetNodeResolutionStateRequest,
|
|
32
|
+
ListParametersOnNodeRequest,
|
|
33
|
+
SetNodeMetadataRequest,
|
|
34
|
+
)
|
|
35
|
+
from griptape_nodes.retained_mode.events.parameter_events import (
|
|
36
|
+
GetParameterValueRequest,
|
|
37
|
+
SetParameterValueRequest,
|
|
38
|
+
)
|
|
39
|
+
from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
|
|
40
|
+
from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
|
|
41
|
+
|
|
42
|
+
SUPPORTED_REQUEST_EVENTS: dict[str, type[RequestPayload]] = {
|
|
43
|
+
# Nodes
|
|
44
|
+
"CreateNodeRequest": CreateNodeRequest,
|
|
45
|
+
"DeleteNodeRequest": DeleteNodeRequest,
|
|
46
|
+
"ListNodesInFlowRequest": ListNodesInFlowRequest,
|
|
47
|
+
"GetNodeResolutionStateRequest": GetNodeResolutionStateRequest,
|
|
48
|
+
"GetNodeMetadataRequest": GetNodeMetadataRequest,
|
|
49
|
+
"SetNodeMetadataRequest": SetNodeMetadataRequest,
|
|
50
|
+
# Connections
|
|
51
|
+
"CreateConnectionRequest": CreateConnectionRequest,
|
|
52
|
+
"DeleteConnectionRequest": DeleteConnectionRequest,
|
|
53
|
+
"ListConnectionsForNodeRequest": ListConnectionsForNodeRequest,
|
|
54
|
+
# Parameters
|
|
55
|
+
"ListParametersOnNodeRequest": ListParametersOnNodeRequest,
|
|
56
|
+
"GetParameterValueRequest": GetParameterValueRequest,
|
|
57
|
+
"SetParameterValueRequest": SetParameterValueRequest,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
GTN_MCP_SERVER_PORT = int(os.getenv("GTN_MCP_SERVER_PORT", "9927"))
|
|
61
|
+
|
|
62
|
+
config_manager = ConfigManager()
|
|
63
|
+
secrets_manager = SecretsManager(config_manager)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main(api_key: str) -> None:
|
|
67
|
+
"""Main entry point for the Griptape Nodes MCP server."""
|
|
68
|
+
mcp_server_logger = logging.getLogger("griptape_nodes_mcp_server")
|
|
69
|
+
mcp_server_logger.addHandler(RichHandler(show_time=True, show_path=False, markup=True, rich_tracebacks=True))
|
|
70
|
+
mcp_server_logger.setLevel(logging.INFO)
|
|
71
|
+
mcp_server_logger.info("Starting MCP GTN server...")
|
|
72
|
+
|
|
73
|
+
# Give these a session ID
|
|
74
|
+
connection_manager = WebSocketConnectionManager()
|
|
75
|
+
request_manager = AsyncRequestManager(connection_manager, api_key)
|
|
76
|
+
|
|
77
|
+
app = Server("mcp-gtn")
|
|
78
|
+
|
|
79
|
+
@app.list_tools()
|
|
80
|
+
async def list_tools() -> list[Tool]:
|
|
81
|
+
return [
|
|
82
|
+
Tool(name=event.__name__, description=event.__doc__, inputSchema=TypeAdapter(event).json_schema())
|
|
83
|
+
for (name, event) in SUPPORTED_REQUEST_EVENTS.items()
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
@app.call_tool()
|
|
87
|
+
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
88
|
+
if name not in SUPPORTED_REQUEST_EVENTS:
|
|
89
|
+
msg = f"Unsupported tool: {name}"
|
|
90
|
+
raise ValueError(msg)
|
|
91
|
+
|
|
92
|
+
request_payload = SUPPORTED_REQUEST_EVENTS[name](**arguments)
|
|
93
|
+
|
|
94
|
+
await request_manager.connect()
|
|
95
|
+
result = await request_manager.create_request_event(
|
|
96
|
+
request_payload.__class__.__name__, request_payload.__dict__, timeout_ms=5000
|
|
97
|
+
)
|
|
98
|
+
mcp_server_logger.debug("Got result: %s", result)
|
|
99
|
+
|
|
100
|
+
return [TextContent(type="text", text=json.dumps(result))]
|
|
101
|
+
|
|
102
|
+
# Create the session manager with our app and event store
|
|
103
|
+
session_manager = StreamableHTTPSessionManager(
|
|
104
|
+
app=app,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@contextlib.asynccontextmanager
|
|
108
|
+
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
|
109
|
+
"""Context manager for managing session manager lifecycle."""
|
|
110
|
+
async with session_manager.run():
|
|
111
|
+
mcp_server_logger.info("GTN MCP server started with StreamableHTTP session manager!")
|
|
112
|
+
try:
|
|
113
|
+
yield
|
|
114
|
+
finally:
|
|
115
|
+
mcp_server_logger.info("GTN MCP server shutting down...")
|
|
116
|
+
|
|
117
|
+
# Create an ASGI application using the transport
|
|
118
|
+
mcp_server_app = FastAPI(lifespan=lifespan)
|
|
119
|
+
|
|
120
|
+
# ASGI handler for streamable HTTP connections
|
|
121
|
+
async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
|
|
122
|
+
await session_manager.handle_request(scope, receive, send)
|
|
123
|
+
|
|
124
|
+
mcp_server_app.mount("/mcp", app=handle_streamable_http)
|
|
125
|
+
|
|
126
|
+
uvicorn.run(mcp_server_app, host="127.0.0.1", port=GTN_MCP_SERVER_PORT)
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import uuid
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any, Generic, TypeVar
|
|
9
|
+
from urllib.parse import urljoin
|
|
10
|
+
|
|
11
|
+
import websockets
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("griptape_nodes_mcp_server")
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WebSocketConnectionManager:
|
|
19
|
+
"""Python equivalent of WebSocketConnectionManager in TypeScript."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
websocket_url: str = urljoin(
|
|
24
|
+
os.getenv("GRIPTAPE_NODES_API_BASE_URL", "https://api.nodes.griptape.ai").replace("http", "ws"),
|
|
25
|
+
"/ws/engines/events?version=v2",
|
|
26
|
+
),
|
|
27
|
+
):
|
|
28
|
+
self.websocket_url = websocket_url
|
|
29
|
+
self.websocket: Any = None
|
|
30
|
+
self.connected = False
|
|
31
|
+
self.event_handlers: dict[str, list[Callable]] = {}
|
|
32
|
+
self.request_handlers: dict[str, tuple[Callable, Callable]] = {}
|
|
33
|
+
self._process_task: asyncio.Task | None = None
|
|
34
|
+
|
|
35
|
+
async def send(self, data: dict[str, Any]) -> None:
|
|
36
|
+
"""Send a message to the WebSocket server."""
|
|
37
|
+
if not self.websocket:
|
|
38
|
+
msg = "Not connected to WebSocket server"
|
|
39
|
+
raise ConnectionError(msg)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
message = json.dumps(data)
|
|
43
|
+
await self.websocket.send(message)
|
|
44
|
+
logger.debug("📤 Sent message: %s", message)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.error("Failed to send message: %s", e)
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
async def _process_messages(self) -> None:
|
|
50
|
+
"""Process incoming WebSocket messages."""
|
|
51
|
+
if not self.websocket:
|
|
52
|
+
logger.warning("WebSocket is not connected, cannot process messages")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
async for message in self.websocket:
|
|
57
|
+
try:
|
|
58
|
+
data = json.loads(message)
|
|
59
|
+
logger.debug("📥 Received message: %s", message)
|
|
60
|
+
await self._handle_message(data)
|
|
61
|
+
except json.JSONDecodeError:
|
|
62
|
+
logger.error("Failed to parse message: %s", message)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error("Error processing message: %s", e)
|
|
65
|
+
except websockets.exceptions.ConnectionClosed:
|
|
66
|
+
logger.warning("WebSocket connection closed")
|
|
67
|
+
self.connected = False
|
|
68
|
+
except asyncio.CancelledError:
|
|
69
|
+
# Task was cancelled, just exit
|
|
70
|
+
pass
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error("Error in message processing loop: %s", e)
|
|
73
|
+
self.connected = False
|
|
74
|
+
|
|
75
|
+
async def _handle_message(self, data: dict[str, Any]) -> None:
|
|
76
|
+
request = data.get("payload", {}).get("request", {})
|
|
77
|
+
request_id = request.get("request_id")
|
|
78
|
+
|
|
79
|
+
if request_id and request_id in self.request_handlers:
|
|
80
|
+
success_handler, failure_handler = self.request_handlers[request_id]
|
|
81
|
+
try:
|
|
82
|
+
if data.get("type") == "success_result":
|
|
83
|
+
success_handler(data, request)
|
|
84
|
+
else:
|
|
85
|
+
failure_handler(data, request)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error("Error in request handler: %s", e)
|
|
88
|
+
|
|
89
|
+
def subscribe_to_request_event(
|
|
90
|
+
self, success_handler: Callable[[Any, Any], None], failure_handler: Callable[[Any, Any], None]
|
|
91
|
+
) -> str:
|
|
92
|
+
"""Subscribe to a request-response event."""
|
|
93
|
+
request_id = str(uuid.uuid4())
|
|
94
|
+
self.request_handlers[request_id] = (success_handler, failure_handler)
|
|
95
|
+
return request_id
|
|
96
|
+
|
|
97
|
+
def unsubscribe_from_request_event(self, request_id: str) -> None:
|
|
98
|
+
"""Unsubscribe from a request-response event."""
|
|
99
|
+
if request_id in self.request_handlers:
|
|
100
|
+
del self.request_handlers[request_id]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AsyncRequestManager(Generic[T]): # noqa: UP046
|
|
104
|
+
def __init__(self, connection_manager: WebSocketConnectionManager, api_key: str):
|
|
105
|
+
self.connection_manager = connection_manager
|
|
106
|
+
self.api_key = api_key
|
|
107
|
+
|
|
108
|
+
async def _subscribe_to_topic(self, topic: str) -> None:
|
|
109
|
+
"""Subscribe to a specific topic in the message bus."""
|
|
110
|
+
if self.connection_manager.websocket is None:
|
|
111
|
+
logger.warning("WebSocket connection not available for subscribing to topic")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
body = {"type": "subscribe", "topic": topic, "payload": {}}
|
|
116
|
+
await self.connection_manager.websocket.send(json.dumps(body))
|
|
117
|
+
logger.debug("Subscribed to topic: %s", topic)
|
|
118
|
+
except websockets.exceptions.WebSocketException as e:
|
|
119
|
+
logger.error("Error subscribing to topic %s: %s", topic, e)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error("Unexpected error while subscribing to topic %s: %s", topic, e)
|
|
122
|
+
|
|
123
|
+
async def _subscribe_to_topics(self) -> None:
|
|
124
|
+
from griptape_nodes.retained_mode.managers.session_manager import SessionManager
|
|
125
|
+
from griptape_nodes.retained_mode.utils.engine_identity import EngineIdentity
|
|
126
|
+
|
|
127
|
+
# Subscribe to response topic (engine discovery)
|
|
128
|
+
await self._subscribe_to_topic("response")
|
|
129
|
+
|
|
130
|
+
# Get engine ID and subscribe to engine_id/response
|
|
131
|
+
engine_id = EngineIdentity.get_engine_id()
|
|
132
|
+
if engine_id:
|
|
133
|
+
await self._subscribe_to_topic(f"engines/{engine_id}/response")
|
|
134
|
+
else:
|
|
135
|
+
logger.warning("Engine ID not available for subscription")
|
|
136
|
+
|
|
137
|
+
# Get session ID and subscribe to session_id/response if available
|
|
138
|
+
session_id = SessionManager.get_saved_session_id()
|
|
139
|
+
if session_id:
|
|
140
|
+
topic = f"sessions/{session_id}/response"
|
|
141
|
+
await self._subscribe_to_topic(topic)
|
|
142
|
+
else:
|
|
143
|
+
logger.info("No session ID available for subscription")
|
|
144
|
+
|
|
145
|
+
async def connect(self) -> None:
|
|
146
|
+
"""Connect to the WebSocket server."""
|
|
147
|
+
from griptape_nodes.app.app import _create_websocket_connection
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
self.connection_manager.websocket = await _create_websocket_connection(self.api_key)
|
|
151
|
+
logger.debug("🟢 WebSocket connection established: %s", self.connection_manager.websocket)
|
|
152
|
+
|
|
153
|
+
await self._subscribe_to_topics()
|
|
154
|
+
|
|
155
|
+
# Start processing messages
|
|
156
|
+
self.connection_manager._process_task = asyncio.create_task(self.connection_manager._process_messages())
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
self.connection_manager.connected = False
|
|
160
|
+
logger.error("🔴 WebSocket connection failed: %s", str(e))
|
|
161
|
+
msg = f"Failed to connect to WebSocket: {e!s}"
|
|
162
|
+
raise ConnectionError(msg) from e
|
|
163
|
+
|
|
164
|
+
async def disconnect(self) -> None:
|
|
165
|
+
"""Disconnect from the WebSocket server."""
|
|
166
|
+
if self.connection_manager.websocket:
|
|
167
|
+
await self.connection_manager.websocket.close()
|
|
168
|
+
self.connection_manager.websocket = None
|
|
169
|
+
self.connection_manager.connected = False
|
|
170
|
+
|
|
171
|
+
# Cancel processing task if it's running
|
|
172
|
+
if self.connection_manager._process_task:
|
|
173
|
+
self.connection_manager._process_task.cancel()
|
|
174
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
175
|
+
await self.connection_manager._process_task
|
|
176
|
+
self.connection_manager._process_task = None
|
|
177
|
+
|
|
178
|
+
logger.debug("WebSocket disconnected")
|
|
179
|
+
|
|
180
|
+
async def send_api_message(self, data: dict[str, Any]) -> None:
|
|
181
|
+
"""Send a message to the API via WebSocket."""
|
|
182
|
+
try:
|
|
183
|
+
await self.connection_manager.send(data)
|
|
184
|
+
except ConnectionError as e:
|
|
185
|
+
logger.error("Failed to send API message: %s", e)
|
|
186
|
+
raise
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error("Unexpected error sending API message: %s", e)
|
|
189
|
+
raise
|
|
190
|
+
|
|
191
|
+
async def create_event(self, request_type: str, payload: dict[str, Any]) -> None:
|
|
192
|
+
"""Send an event to the API without waiting for a response."""
|
|
193
|
+
from griptape_nodes.app.app import _determine_request_topic
|
|
194
|
+
|
|
195
|
+
logger.debug("📝 Creating Event: %s - %s", request_type, json.dumps(payload))
|
|
196
|
+
|
|
197
|
+
data = {"event_type": "EventRequest", "request_type": request_type, "request": payload}
|
|
198
|
+
topic = _determine_request_topic()
|
|
199
|
+
|
|
200
|
+
request_data = {"payload": data, "type": data["event_type"], "topic": topic}
|
|
201
|
+
|
|
202
|
+
if not request_data["payload"]["request"].get("request_id"):
|
|
203
|
+
request_data["payload"]["request"]["request_id"] = ""
|
|
204
|
+
|
|
205
|
+
await self.send_api_message(request_data)
|
|
206
|
+
|
|
207
|
+
async def create_request_event(
|
|
208
|
+
self, request_type: str, payload: dict[str, Any], timeout_ms: int | None = None
|
|
209
|
+
) -> T:
|
|
210
|
+
"""Send a request and wait for its response.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
request_type: Type of request to send
|
|
214
|
+
payload: Data to send with the request
|
|
215
|
+
timeout_ms: Optional timeout in milliseconds
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
The response data
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
asyncio.TimeoutError: If the request times out
|
|
222
|
+
Exception: If the request fails
|
|
223
|
+
"""
|
|
224
|
+
# Create a future that will be resolved when the response arrives
|
|
225
|
+
response_future = asyncio.Future()
|
|
226
|
+
|
|
227
|
+
# Convert timeout from milliseconds to seconds for asyncio
|
|
228
|
+
timeout_sec = timeout_ms / 1000 if timeout_ms else None
|
|
229
|
+
|
|
230
|
+
# Define handlers that will resolve/reject the future
|
|
231
|
+
def success_handler(response: Any, _: Any) -> None:
|
|
232
|
+
if not response_future.done():
|
|
233
|
+
result = response.get("payload", {}).get("result", "Success")
|
|
234
|
+
logger.debug("✅ Request succeeded: %s", result)
|
|
235
|
+
response_future.set_result(result)
|
|
236
|
+
|
|
237
|
+
def failure_handler(response: Any, _: Any) -> None:
|
|
238
|
+
if not response_future.done():
|
|
239
|
+
error = (
|
|
240
|
+
response.get("payload", {}).get("result", {}).get("exception", "Unknown error") or "Unknown error"
|
|
241
|
+
)
|
|
242
|
+
logger.error("❌ Request failed: %s", error)
|
|
243
|
+
response_future.set_exception(Exception(error))
|
|
244
|
+
|
|
245
|
+
# Generate request ID and subscribe
|
|
246
|
+
request_id = self.connection_manager.subscribe_to_request_event(success_handler, failure_handler)
|
|
247
|
+
payload["request_id"] = request_id
|
|
248
|
+
|
|
249
|
+
logger.debug("🚀 Request (%s): %s %s", request_id, request_type, json.dumps(payload))
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
# Send the event
|
|
253
|
+
await self.create_event(request_type, payload)
|
|
254
|
+
|
|
255
|
+
# Wait for the response with optional timeout
|
|
256
|
+
if timeout_sec:
|
|
257
|
+
return await asyncio.wait_for(response_future, timeout=timeout_sec)
|
|
258
|
+
return await response_future
|
|
259
|
+
|
|
260
|
+
except TimeoutError:
|
|
261
|
+
logger.error("Request timed out after %s ms: %s", timeout_ms, request_id)
|
|
262
|
+
self.connection_manager.unsubscribe_from_request_event(request_id)
|
|
263
|
+
raise
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error("Request failed: %s - %s", request_id, e)
|
|
267
|
+
self.connection_manager.unsubscribe_from_request_event(request_id)
|
|
268
|
+
raise
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -13,19 +13,19 @@ from griptape_nodes.utils.metaclasses import SingletonMeta
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class WorkflowMetadata(BaseModel):
|
|
16
|
-
LATEST_SCHEMA_VERSION: ClassVar[str] = "0.
|
|
16
|
+
LATEST_SCHEMA_VERSION: ClassVar[str] = "0.6.1"
|
|
17
17
|
|
|
18
18
|
name: str
|
|
19
19
|
schema_version: str
|
|
20
20
|
engine_version_created_with: str
|
|
21
21
|
node_libraries_referenced: list[LibraryNameAndVersion]
|
|
22
|
+
workflows_referenced: list[str] | None = None
|
|
22
23
|
description: str | None = None
|
|
23
24
|
image: str | None = None
|
|
24
25
|
is_griptape_provided: bool | None = False
|
|
25
26
|
is_template: bool | None = False
|
|
26
27
|
creation_date: datetime | None = Field(default=None)
|
|
27
28
|
last_modified_date: datetime | None = Field(default=None)
|
|
28
|
-
published_workflow_id: str | None = Field(default=None)
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class WorkflowRegistry(metaclass=SingletonMeta):
|
griptape_nodes/py.typed
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|