griptape-nodes 0.51.2__py3-none-any.whl → 0.52.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.
Files changed (46) hide show
  1. griptape_nodes/__init__.py +5 -4
  2. griptape_nodes/app/api.py +22 -30
  3. griptape_nodes/app/app.py +374 -289
  4. griptape_nodes/app/watch.py +17 -2
  5. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +66 -103
  6. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +16 -4
  7. griptape_nodes/exe_types/core_types.py +16 -4
  8. griptape_nodes/exe_types/node_types.py +74 -16
  9. griptape_nodes/machines/control_flow.py +21 -26
  10. griptape_nodes/machines/fsm.py +16 -16
  11. griptape_nodes/machines/node_resolution.py +30 -119
  12. griptape_nodes/mcp_server/server.py +14 -10
  13. griptape_nodes/mcp_server/ws_request_manager.py +2 -2
  14. griptape_nodes/node_library/workflow_registry.py +5 -0
  15. griptape_nodes/retained_mode/events/base_events.py +12 -7
  16. griptape_nodes/retained_mode/events/execution_events.py +0 -6
  17. griptape_nodes/retained_mode/events/node_events.py +38 -0
  18. griptape_nodes/retained_mode/events/parameter_events.py +11 -0
  19. griptape_nodes/retained_mode/events/variable_events.py +361 -0
  20. griptape_nodes/retained_mode/events/workflow_events.py +35 -0
  21. griptape_nodes/retained_mode/griptape_nodes.py +61 -26
  22. griptape_nodes/retained_mode/managers/agent_manager.py +8 -9
  23. griptape_nodes/retained_mode/managers/event_manager.py +215 -74
  24. griptape_nodes/retained_mode/managers/flow_manager.py +39 -33
  25. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +14 -14
  26. griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +20 -20
  27. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +1 -1
  28. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +1 -1
  29. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +4 -3
  30. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +1 -1
  31. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +1 -1
  32. griptape_nodes/retained_mode/managers/library_manager.py +20 -19
  33. griptape_nodes/retained_mode/managers/node_manager.py +83 -8
  34. griptape_nodes/retained_mode/managers/object_manager.py +4 -0
  35. griptape_nodes/retained_mode/managers/settings.py +1 -0
  36. griptape_nodes/retained_mode/managers/sync_manager.py +3 -9
  37. griptape_nodes/retained_mode/managers/variable_manager.py +529 -0
  38. griptape_nodes/retained_mode/managers/workflow_manager.py +156 -50
  39. griptape_nodes/retained_mode/variable_types.py +18 -0
  40. griptape_nodes/utils/__init__.py +4 -0
  41. griptape_nodes/utils/async_utils.py +89 -0
  42. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.1.dist-info}/METADATA +2 -3
  43. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.1.dist-info}/RECORD +45 -42
  44. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.1.dist-info}/WHEEL +1 -1
  45. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +0 -90
  46. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.1.dist-info}/entry_points.txt +0 -0
@@ -36,9 +36,7 @@ from griptape_nodes.retained_mode.events.app_events import (
36
36
  SetEngineNameResultSuccess,
37
37
  )
38
38
  from griptape_nodes.retained_mode.events.base_events import (
39
- AppPayload,
40
- RequestPayload,
41
- ResultPayload,
39
+ GriptapeNodeEvent,
42
40
  ResultPayloadFailure,
43
41
  )
44
42
  from griptape_nodes.retained_mode.events.flow_events import (
@@ -48,6 +46,11 @@ from griptape_nodes.utils.metaclasses import SingletonMeta
48
46
  from griptape_nodes.utils.version_utils import engine_version
49
47
 
50
48
  if TYPE_CHECKING:
49
+ from griptape_nodes.retained_mode.events.base_events import (
50
+ AppPayload,
51
+ RequestPayload,
52
+ ResultPayload,
53
+ )
51
54
  from griptape_nodes.retained_mode.managers.agent_manager import AgentManager
52
55
  from griptape_nodes.retained_mode.managers.arbitrary_code_exec_manager import (
53
56
  ArbitraryCodeExecManager,
@@ -70,6 +73,9 @@ if TYPE_CHECKING:
70
73
  StaticFilesManager,
71
74
  )
72
75
  from griptape_nodes.retained_mode.managers.sync_manager import SyncManager
76
+ from griptape_nodes.retained_mode.managers.variable_manager import (
77
+ VariablesManager,
78
+ )
73
79
  from griptape_nodes.retained_mode.managers.version_compatibility_manager import (
74
80
  VersionCompatibilityManager,
75
81
  )
@@ -132,6 +138,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
132
138
  _context_manager: ContextManager
133
139
  _library_manager: LibraryManager
134
140
  _workflow_manager: WorkflowManager
141
+ _workflow_variables_manager: VariablesManager
135
142
  _arbitrary_code_exec_manager: ArbitraryCodeExecManager
136
143
  _operation_depth_manager: OperationDepthManager
137
144
  _static_files_manager: StaticFilesManager
@@ -164,6 +171,9 @@ class GriptapeNodes(metaclass=SingletonMeta):
164
171
  StaticFilesManager,
165
172
  )
166
173
  from griptape_nodes.retained_mode.managers.sync_manager import SyncManager
174
+ from griptape_nodes.retained_mode.managers.variable_manager import (
175
+ VariablesManager,
176
+ )
167
177
  from griptape_nodes.retained_mode.managers.version_compatibility_manager import (
168
178
  VersionCompatibilityManager,
169
179
  )
@@ -183,6 +193,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
183
193
  self._context_manager = ContextManager(self._event_manager)
184
194
  self._library_manager = LibraryManager(self._event_manager)
185
195
  self._workflow_manager = WorkflowManager(self._event_manager)
196
+ self._workflow_variables_manager = VariablesManager(self._event_manager)
186
197
  self._arbitrary_code_exec_manager = ArbitraryCodeExecManager(self._event_manager)
187
198
  self._operation_depth_manager = OperationDepthManager(self._config_manager)
188
199
  self._static_files_manager = StaticFilesManager(
@@ -226,33 +237,53 @@ class GriptapeNodes(metaclass=SingletonMeta):
226
237
  def handle_request(
227
238
  cls,
228
239
  request: RequestPayload,
229
- *,
230
- response_topic: str | None = None,
231
- request_id: str | None = None,
232
240
  ) -> ResultPayload:
241
+ """Synchronous request handler."""
233
242
  event_mgr = GriptapeNodes.EventManager()
234
- obj_depth_mgr = GriptapeNodes.OperationDepthManager()
235
- workflow_mgr = GriptapeNodes.WorkflowManager()
243
+
236
244
  try:
237
- return event_mgr.handle_request(
238
- request=request,
239
- operation_depth_mgr=obj_depth_mgr,
240
- workflow_mgr=workflow_mgr,
241
- response_topic=response_topic,
242
- request_id=request_id,
243
- )
245
+ result_event = event_mgr.handle_request(request=request)
246
+ event_mgr.put_event(GriptapeNodeEvent(wrapped_event=result_event))
244
247
  except Exception as e:
245
248
  logger.exception(
246
249
  "Unhandled exception while processing request of type %s. "
247
- "Consider saving your work and restarting the engine if issues persist.",
250
+ "Consider saving your work and restarting the engine if issues persist."
251
+ "Request: %s",
248
252
  type(request).__name__,
253
+ request,
249
254
  )
250
255
  return ResultPayloadFailure(exception=e)
256
+ else:
257
+ return result_event.result
251
258
 
252
259
  @classmethod
253
- def broadcast_app_event(cls, app_event: AppPayload) -> None:
260
+ async def ahandle_request(cls, request: RequestPayload) -> ResultPayload:
261
+ """Asynchronous request handler.
262
+
263
+ Args:
264
+ request: The request payload to handle.
265
+ """
266
+ event_mgr = GriptapeNodes.EventManager()
267
+
268
+ try:
269
+ result_event = await event_mgr.ahandle_request(request=request)
270
+ await event_mgr.aput_event(GriptapeNodeEvent(wrapped_event=result_event))
271
+ except Exception as e:
272
+ logger.exception(
273
+ "Unhandled exception while processing async request of type %s. "
274
+ "Consider saving your work and restarting the engine if issues persist."
275
+ "Request: %s",
276
+ type(request).__name__,
277
+ request,
278
+ )
279
+ return ResultPayloadFailure(exception=e)
280
+ else:
281
+ return result_event.result
282
+
283
+ @classmethod
284
+ async def broadcast_app_event(cls, app_event: AppPayload) -> None:
254
285
  event_mgr = GriptapeNodes.get_instance()._event_manager
255
- return event_mgr.broadcast_app_event(app_event)
286
+ await event_mgr.broadcast_app_event(app_event)
256
287
 
257
288
  @classmethod
258
289
  def get_session_id(cls) -> str | None:
@@ -334,6 +365,10 @@ class GriptapeNodes(metaclass=SingletonMeta):
334
365
  def SyncManager(cls) -> SyncManager:
335
366
  return GriptapeNodes.get_instance()._sync_manager
336
367
 
368
+ @classmethod
369
+ def VariablesManager(cls) -> VariablesManager:
370
+ return GriptapeNodes.get_instance()._workflow_variables_manager
371
+
337
372
  @classmethod
338
373
  def clear_data(cls) -> None:
339
374
  # Get canvas
@@ -357,16 +392,16 @@ class GriptapeNodes(metaclass=SingletonMeta):
357
392
  msg = "Failed to successfully delete all objects"
358
393
  raise ValueError(msg)
359
394
 
360
- def on_app_connection_established(self, _payload: AppConnectionEstablished) -> None:
395
+ async def on_app_connection_established(self, _payload: AppConnectionEstablished) -> None:
361
396
  from griptape_nodes.app.app import subscribe_to_topic
362
397
 
363
398
  # Subscribe to request topic (engine discovery)
364
- subscribe_to_topic("request")
399
+ await subscribe_to_topic("request")
365
400
 
366
401
  # Get engine ID and subscribe to engine_id/request
367
402
  engine_id = GriptapeNodes.get_engine_id()
368
403
  if engine_id:
369
- subscribe_to_topic(f"engines/{engine_id}/request")
404
+ await subscribe_to_topic(f"engines/{engine_id}/request")
370
405
  else:
371
406
  logger.warning("Engine ID not available for subscription")
372
407
 
@@ -374,7 +409,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
374
409
  session_id = GriptapeNodes.get_session_id()
375
410
  if session_id:
376
411
  topic = f"sessions/{session_id}/request"
377
- subscribe_to_topic(topic)
412
+ await subscribe_to_topic(topic)
378
413
  else:
379
414
  logger.info("No session ID available for subscription")
380
415
 
@@ -395,7 +430,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
395
430
  logger.error(details)
396
431
  return GetEngineVersionResultFailure(result_details=details)
397
432
 
398
- def handle_session_start_request(self, request: AppStartSessionRequest) -> ResultPayload: # noqa: ARG002
433
+ async def handle_session_start_request(self, request: AppStartSessionRequest) -> ResultPayload: # noqa: ARG002
399
434
  from griptape_nodes.app.app import subscribe_to_topic
400
435
 
401
436
  current_session_id = GriptapeNodes.SessionManager().get_active_session_id()
@@ -409,12 +444,12 @@ class GriptapeNodes(metaclass=SingletonMeta):
409
444
  details = f"Session '{current_session_id}' already active. Joining..."
410
445
 
411
446
  topic = f"sessions/{current_session_id}/request"
412
- subscribe_to_topic(topic)
447
+ await subscribe_to_topic(topic)
413
448
  logger.info("Subscribed to new session topic: %s", topic)
414
449
 
415
450
  return AppStartSessionResultSuccess(current_session_id)
416
451
 
417
- def handle_session_end_request(self, _: AppEndSessionRequest) -> ResultPayload:
452
+ async def handle_session_end_request(self, _: AppEndSessionRequest) -> ResultPayload:
418
453
  from griptape_nodes.app.app import unsubscribe_from_topic
419
454
 
420
455
  try:
@@ -428,7 +463,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
428
463
  GriptapeNodes.SessionManager().clear_saved_session()
429
464
 
430
465
  unsubscribe_topic = f"sessions/{previous_session_id}/request"
431
- unsubscribe_from_topic(unsubscribe_topic)
466
+ await unsubscribe_from_topic(unsubscribe_topic)
432
467
 
433
468
  return AppEndSessionResultSuccess(session_id=previous_session_id)
434
469
  except Exception as err:
@@ -1,7 +1,7 @@
1
+ import asyncio
1
2
  import json
2
3
  import logging
3
4
  import os
4
- import threading
5
5
  import uuid
6
6
  from typing import TYPE_CHECKING
7
7
 
@@ -10,7 +10,7 @@ from griptape.artifacts import ErrorArtifact, ImageUrlArtifact, JsonArtifact
10
10
  from griptape.drivers.image_generation import BaseImageGenerationDriver
11
11
  from griptape.drivers.image_generation.griptape_cloud import GriptapeCloudImageGenerationDriver
12
12
  from griptape.drivers.prompt.griptape_cloud import GriptapeCloudPromptDriver
13
- from griptape.events import EventBus, EventListener, FinishTaskEvent, TextChunkEvent
13
+ from griptape.events import FinishTaskEvent, TextChunkEvent
14
14
  from griptape.loaders import ImageLoader
15
15
  from griptape.memory.structure import ConversationMemory
16
16
  from griptape.rules import Rule, Ruleset
@@ -38,6 +38,7 @@ from griptape_nodes.retained_mode.events.agent_events import (
38
38
  RunAgentResultSuccess,
39
39
  )
40
40
  from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, ExecutionGriptapeNodeEvent, ResultPayload
41
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
41
42
  from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
42
43
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
43
44
  from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
@@ -128,14 +129,14 @@ class AgentManager:
128
129
  }
129
130
  return MCPTool(connection=connection)
130
131
 
131
- def on_handle_run_agent_request(self, request: RunAgentRequest) -> ResultPayload:
132
+ async def on_handle_run_agent_request(self, request: RunAgentRequest) -> ResultPayload:
132
133
  if self.prompt_driver is None:
133
134
  self.prompt_driver = self._initialize_prompt_driver()
134
135
  if self.image_tool is None:
135
136
  self.image_tool = self._initialize_image_tool()
136
137
  if self.mcp_tool is None:
137
138
  self.mcp_tool = self._initialize_mcp_tool()
138
- threading.Thread(target=self._on_handle_run_agent_request, args=(request, EventBus.event_listeners)).start()
139
+ await asyncio.to_thread(self._on_handle_run_agent_request, request)
139
140
  return RunAgentResultStarted()
140
141
 
141
142
  def _create_agent(self) -> Agent:
@@ -168,10 +169,8 @@ class AgentManager:
168
169
  ],
169
170
  )
170
171
 
171
- def _on_handle_run_agent_request(
172
- self, request: RunAgentRequest, event_listeners: list[EventListener]
173
- ) -> ResultPayload:
174
- EventBus.event_listeners = event_listeners
172
+ def _on_handle_run_agent_request(self, request: RunAgentRequest) -> ResultPayload:
173
+ # EventBus functionality removed - events now go directly to event queue
175
174
  try:
176
175
  artifacts = [
177
176
  ImageLoader().parse(ImageUrlArtifact.from_dict(url_artifact).to_bytes())
@@ -190,7 +189,7 @@ class AgentManager:
190
189
  if "conversation_output" in result_json:
191
190
  new_conversation_output = result_json["conversation_output"]
192
191
  if new_conversation_output != last_conversation_output:
193
- EventBus.publish_event(
192
+ GriptapeNodes.EventManager().put_event(
194
193
  ExecutionGriptapeNodeEvent(
195
194
  wrapped_event=ExecutionEvent(
196
195
  payload=AgentStreamEvent(
@@ -1,10 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
1
5
  from collections import defaultdict
2
- from collections.abc import Callable
3
6
  from dataclasses import fields
4
- from typing import TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, Any, cast
5
8
 
6
- from griptape.events import EventBus
7
- from typing_extensions import TypeVar
9
+ from typing_extensions import TypedDict, TypeVar
8
10
 
9
11
  from griptape_nodes.retained_mode.events.base_events import (
10
12
  AppPayload,
@@ -12,37 +14,99 @@ from griptape_nodes.retained_mode.events.base_events import (
12
14
  EventResultFailure,
13
15
  EventResultSuccess,
14
16
  FlushParameterChangesRequest,
15
- GriptapeNodeEvent,
16
17
  RequestPayload,
17
18
  ResultPayload,
18
19
  WorkflowAlteredMixin,
19
20
  )
21
+ from griptape_nodes.utils.async_utils import call_function
20
22
 
21
23
  if TYPE_CHECKING:
22
- from griptape_nodes.retained_mode.griptape_nodes import WorkflowManager
23
- from griptape_nodes.retained_mode.managers.operation_manager import OperationDepthManager
24
+ from collections.abc import Awaitable, Callable
25
+
24
26
 
25
27
  RP = TypeVar("RP", bound=RequestPayload, default=RequestPayload)
26
28
  AP = TypeVar("AP", bound=AppPayload, default=AppPayload)
27
29
 
28
30
 
31
+ class ResultContext(TypedDict, total=False):
32
+ response_topic: str | None
33
+ request_id: str | None
34
+
35
+
29
36
  class EventManager:
30
37
  def __init__(self) -> None:
31
38
  # Dictionary to store the SPECIFIC manager for each request type
32
39
  self._request_type_to_manager: dict[type[RequestPayload], Callable] = defaultdict(list) # pyright: ignore[reportAttributeAccessIssue]
33
40
  # Dictionary to store ALL SUBSCRIBERS to app events.
34
41
  self._app_event_listeners: dict[type[AppPayload], set[Callable]] = {}
35
- self.current_active_node: str | None = None
36
42
  # Boolean that lets us know if there is currently a FlushParameterChangesRequest in the event queue.
37
43
  self._flush_in_queue: bool = False
44
+ # Event queue for publishing events
45
+ self._event_queue: asyncio.Queue | None = None
46
+
47
+ @property
48
+ def event_queue(self) -> asyncio.Queue:
49
+ if self._event_queue is None:
50
+ msg = "Event queue has not been initialized. Please call 'initialize_queue' with an asyncio.Queue instance before accessing the event queue."
51
+ raise ValueError(msg)
52
+ return self._event_queue
38
53
 
39
54
  def clear_flush_in_queue(self) -> None:
40
55
  self._flush_in_queue = False
41
56
 
57
+ def initialize_queue(self, queue: asyncio.Queue | None = None) -> None:
58
+ """Set the event queue for this manager.
59
+
60
+ Args:
61
+ queue: The asyncio.Queue to use for events, or None to clear
62
+ """
63
+ if queue is not None:
64
+ self._event_queue = queue
65
+ else:
66
+ try:
67
+ self._event_queue = asyncio.Queue()
68
+ except RuntimeError:
69
+ # Defer queue creation until we're in an event loop
70
+ self._event_queue = None
71
+
72
+ def put_event(self, event: Any) -> None:
73
+ """Put event into async queue from sync context (non-blocking).
74
+
75
+ Args:
76
+ event: The event to publish to the queue
77
+ """
78
+ if self._event_queue is None:
79
+ return
80
+
81
+ self._event_queue.put_nowait(event)
82
+
83
+ async def aput_event(self, event: Any) -> None:
84
+ """Put event into async queue from async context.
85
+
86
+ Args:
87
+ event: The event to publish to the queue
88
+ """
89
+ if self._event_queue is None:
90
+ return
91
+
92
+ await self._event_queue.put(event)
93
+
94
+ def put_event_threadsafe(self, loop: Any, event: Any) -> None:
95
+ """Put event into async queue from sync context in a thread-safe manner.
96
+
97
+ Args:
98
+ loop: The asyncio event loop to use for thread-safe operation
99
+ event: The event to publish to the queue
100
+ """
101
+ if self._event_queue is None:
102
+ return
103
+
104
+ loop.call_soon_threadsafe(self._event_queue.put_nowait, event)
105
+
42
106
  def assign_manager_to_request_type(
43
107
  self,
44
108
  request_type: type[RP],
45
- callback: Callable[[RP], ResultPayload],
109
+ callback: Callable[[RP], ResultPayload] | Callable[[RP], Awaitable[ResultPayload]],
46
110
  ) -> None:
47
111
  """Assign a manager to handle a request.
48
112
 
@@ -65,79 +129,154 @@ class EventManager:
65
129
  if request_type in self._request_type_to_manager:
66
130
  del self._request_type_to_manager[request_type]
67
131
 
68
- def handle_request(
132
+ def _handle_request_core(
133
+ self,
134
+ request: RP,
135
+ callback_result: ResultPayload,
136
+ *,
137
+ context: ResultContext,
138
+ ) -> EventResultSuccess | EventResultFailure:
139
+ """Core logic for handling requests, shared between sync and async methods."""
140
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
141
+
142
+ operation_depth_mgr = GriptapeNodes.OperationDepthManager()
143
+ workflow_mgr = GriptapeNodes.WorkflowManager()
144
+
145
+ with operation_depth_mgr as depth_manager:
146
+ # Now see if the WorkflowManager was asking us to squelch altered_workflow_state commands
147
+ # This prevents situations like loading a workflow (which naturally alters the workflow state)
148
+ # from coming in and immediately being flagged as being dirty.
149
+ if workflow_mgr.should_squelch_workflow_altered():
150
+ callback_result.altered_workflow_state = False
151
+
152
+ retained_mode_str = None
153
+ # If request_id exists, that means it's a direct request from the GUI (not internal), and should be echoed by retained mode.
154
+ if depth_manager.is_top_level() and context.get("request_id") is not None:
155
+ retained_mode_str = depth_manager.request_retained_mode_translation(request)
156
+
157
+ # Some requests have fields marked as "omit_from_result" which should be removed from the request
158
+ for field in fields(request):
159
+ if field.metadata.get("omit_from_result", False):
160
+ setattr(request, field.name, None)
161
+ if callback_result.succeeded():
162
+ result_event = EventResultSuccess(
163
+ request=request,
164
+ request_id=context.get("request_id"),
165
+ result=callback_result,
166
+ retained_mode=retained_mode_str,
167
+ response_topic=context.get("response_topic"),
168
+ )
169
+ else:
170
+ result_event = EventResultFailure(
171
+ request=request,
172
+ request_id=context.get("request_id"),
173
+ result=callback_result,
174
+ retained_mode=retained_mode_str,
175
+ response_topic=context.get("response_topic"),
176
+ )
177
+
178
+ return result_event
179
+
180
+ async def ahandle_request(
69
181
  self,
70
182
  request: RP,
71
183
  *,
72
- operation_depth_mgr: "OperationDepthManager",
73
- workflow_mgr: "WorkflowManager",
74
- response_topic: str | None = None,
75
- request_id: str | None = None,
76
- ) -> ResultPayload:
184
+ result_context: ResultContext | None = None,
185
+ ) -> EventResultSuccess | EventResultFailure:
77
186
  """Publish an event to the manager assigned to its type.
78
187
 
79
188
  Args:
80
189
  request: The request to handle
81
- operation_depth_mgr: The operation depth manager to use
82
- workflow_mgr: The workflow manager to use
83
- response_topic: The topic to send the response to (optional)
84
- request_id: The ID of the request to correlate with the response (optional)
190
+ result_context: The result context containing response_topic and request_id
85
191
  """
192
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
193
+
194
+ operation_depth_mgr = GriptapeNodes.OperationDepthManager()
195
+ if result_context is None:
196
+ result_context = ResultContext()
197
+
86
198
  # Notify the manager of the event type
87
- with operation_depth_mgr as depth_manager:
88
- request_type = type(request)
89
- callback = self._request_type_to_manager.get(request_type)
90
- if callback:
91
- # Actually make the handler callback:
92
- result_payload = callback(request)
93
-
94
- # Now see if the WorkflowManager was asking us to squelch altered_workflow_state commands
95
- # This prevents situations like loading a workflow (which naturally alters the workflow state)
96
- # from coming in and immediately being flagged as being dirty.
97
- if workflow_mgr.should_squelch_workflow_altered():
98
- result_payload.altered_workflow_state = False
99
-
100
- retained_mode_str = None
101
- # If request_id exists, that means it's a direct request from the GUI (not internal), and should be echoed by retained mode.
102
- if depth_manager.is_top_level() and request_id is not None:
103
- retained_mode_str = depth_manager.request_retained_mode_translation(request)
104
-
105
- # Some requests have fields marked as "omit_from_result" which should be removed from the request
106
- for field in fields(request):
107
- if field.metadata.get("omit_from_result", False):
108
- setattr(request, field.name, None)
109
- if result_payload.succeeded():
110
- result_event = EventResultSuccess(
111
- request=request,
112
- request_id=request_id,
113
- result=result_payload,
114
- retained_mode=retained_mode_str,
115
- response_topic=response_topic,
116
- )
117
- # If the result is a success, and the WorkflowAlteredMixin is present, that means the flow has been changed in some way.
118
- # In that case, we need to flush the element changes, so we add one to the event queue.
119
- if isinstance(result_event.result, WorkflowAlteredMixin) and not self._flush_in_queue:
120
- from griptape_nodes.app.app import event_queue
121
-
122
- event_queue.put(EventRequest(request=FlushParameterChangesRequest()))
123
- self._flush_in_queue = True
124
- else:
125
- result_event = EventResultFailure(
126
- request=request,
127
- request_id=request_id,
128
- result=result_payload,
129
- retained_mode=retained_mode_str,
130
- response_topic=response_topic,
131
- )
132
- wrapped_event = GriptapeNodeEvent(wrapped_event=result_event)
133
- EventBus.publish_event(wrapped_event)
134
- else:
135
- msg = f"No manager found to handle request of type '{request_type.__name__}."
136
- raise TypeError(msg)
199
+ request_type = type(request)
200
+ callback = self._request_type_to_manager.get(request_type)
201
+ if not callback:
202
+ msg = f"No manager found to handle request of type '{request_type.__name__}'."
203
+ raise TypeError(msg)
204
+
205
+ # Actually make the handler callback (support both sync and async):
206
+ result_payload: ResultPayload = await call_function(callback, request)
207
+
208
+ # Handle workflow alteration events for async context
209
+ with operation_depth_mgr:
210
+ if (
211
+ result_payload.succeeded()
212
+ and isinstance(result_payload, WorkflowAlteredMixin)
213
+ and not self._flush_in_queue
214
+ ):
215
+ await self.aput_event(EventRequest(request=FlushParameterChangesRequest()))
216
+ self._flush_in_queue = True
217
+
218
+ return self._handle_request_core(
219
+ request,
220
+ cast("ResultPayload", result_payload),
221
+ context=result_context,
222
+ )
223
+
224
+ def handle_request(
225
+ self,
226
+ request: RP,
227
+ *,
228
+ result_context: ResultContext | None = None,
229
+ ) -> EventResultSuccess | EventResultFailure:
230
+ """Publish an event to the manager assigned to its type (sync version).
231
+
232
+ Args:
233
+ request: The request to handle
234
+ result_context: The result context containing response_topic and request_id
235
+ """
236
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
137
237
 
138
- return result_payload
238
+ operation_depth_mgr = GriptapeNodes.OperationDepthManager()
239
+ if result_context is None:
240
+ result_context = ResultContext()
139
241
 
140
- def add_listener_to_app_event(self, app_event_type: type[AP], callback: Callable[[AP], None]) -> None:
242
+ # Notify the manager of the event type
243
+ request_type = type(request)
244
+ callback = self._request_type_to_manager.get(request_type)
245
+ if not callback:
246
+ msg = f"No manager found to handle request of type '{request_type.__name__}'."
247
+ raise TypeError(msg)
248
+
249
+ # Support async callbacks for sync method ONLY if there is no running event loop
250
+ if inspect.iscoroutinefunction(callback):
251
+ try:
252
+ asyncio.get_running_loop()
253
+ msg = "Async handler cannot be called with sync handle_request. Use ahandle_request instead."
254
+ raise ValueError(msg)
255
+ except RuntimeError:
256
+ # No event loop running, safe to use asyncio.run
257
+ result_payload: ResultPayload = asyncio.run(callback(request))
258
+ else:
259
+ result_payload: ResultPayload = callback(request)
260
+
261
+ # Handle workflow alteration events for sync context
262
+ with operation_depth_mgr:
263
+ if (
264
+ result_payload.succeeded()
265
+ and isinstance(result_payload, WorkflowAlteredMixin)
266
+ and not self._flush_in_queue
267
+ ):
268
+ self.put_event(EventRequest(request=FlushParameterChangesRequest()))
269
+ self._flush_in_queue = True
270
+
271
+ return self._handle_request_core(
272
+ request,
273
+ cast("ResultPayload", result_payload),
274
+ context=result_context,
275
+ )
276
+
277
+ def add_listener_to_app_event(
278
+ self, app_event_type: type[AP], callback: Callable[[AP], None] | Callable[[AP], Awaitable[None]]
279
+ ) -> None:
141
280
  listener_set = self._app_event_listeners.get(app_event_type)
142
281
  if listener_set is None:
143
282
  listener_set = set()
@@ -145,13 +284,15 @@ class EventManager:
145
284
 
146
285
  listener_set.add(callback)
147
286
 
148
- def remove_listener_for_app_event(self, app_event_type: type[AP], callback: Callable[[AP], None]) -> None:
287
+ def remove_listener_for_app_event(
288
+ self, app_event_type: type[AP], callback: Callable[[AP], None] | Callable[[AP], Awaitable[None]]
289
+ ) -> None:
149
290
  listener_set = self._app_event_listeners[app_event_type]
150
291
  listener_set.remove(callback)
151
292
 
152
- def broadcast_app_event(self, app_event: AP) -> None:
293
+ async def broadcast_app_event(self, app_event: AP) -> None:
153
294
  app_event_type = type(app_event)
154
295
  if app_event_type in self._app_event_listeners:
155
296
  listener_set = self._app_event_listeners[app_event_type]
156
297
  for listener_callback in listener_set:
157
- listener_callback(app_event)
298
+ await call_function(listener_callback, app_event)