griptape-nodes 0.55.1__py3-none-any.whl → 0.56.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- griptape_nodes/app/app.py +10 -15
- griptape_nodes/app/watch.py +35 -67
- griptape_nodes/bootstrap/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/utils/python_subprocess_executor.py +122 -0
- griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +418 -0
- griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +37 -8
- griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +326 -0
- griptape_nodes/bootstrap/workflow_executors/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +51 -0
- griptape_nodes/bootstrap/workflow_publishers/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +43 -0
- griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +84 -0
- griptape_nodes/bootstrap/workflow_publishers/utils/__init__.py +1 -0
- griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +54 -0
- griptape_nodes/cli/commands/engine.py +4 -15
- griptape_nodes/cli/commands/init.py +88 -0
- griptape_nodes/cli/commands/models.py +2 -0
- griptape_nodes/cli/main.py +6 -1
- griptape_nodes/cli/shared.py +1 -0
- griptape_nodes/exe_types/core_types.py +130 -0
- griptape_nodes/exe_types/node_types.py +125 -13
- griptape_nodes/machines/control_flow.py +10 -0
- griptape_nodes/machines/dag_builder.py +21 -2
- griptape_nodes/machines/parallel_resolution.py +25 -10
- griptape_nodes/node_library/workflow_registry.py +73 -3
- griptape_nodes/retained_mode/events/agent_events.py +2 -0
- griptape_nodes/retained_mode/events/base_events.py +18 -17
- griptape_nodes/retained_mode/events/execution_events.py +15 -3
- griptape_nodes/retained_mode/events/flow_events.py +63 -7
- griptape_nodes/retained_mode/events/mcp_events.py +363 -0
- griptape_nodes/retained_mode/events/node_events.py +3 -4
- griptape_nodes/retained_mode/events/resource_events.py +290 -0
- griptape_nodes/retained_mode/events/workflow_events.py +57 -2
- griptape_nodes/retained_mode/griptape_nodes.py +17 -1
- griptape_nodes/retained_mode/managers/agent_manager.py +67 -4
- griptape_nodes/retained_mode/managers/event_manager.py +31 -13
- griptape_nodes/retained_mode/managers/flow_manager.py +731 -33
- griptape_nodes/retained_mode/managers/library_manager.py +15 -23
- griptape_nodes/retained_mode/managers/mcp_manager.py +364 -0
- griptape_nodes/retained_mode/managers/model_manager.py +184 -83
- griptape_nodes/retained_mode/managers/node_manager.py +15 -4
- griptape_nodes/retained_mode/managers/os_manager.py +118 -1
- griptape_nodes/retained_mode/managers/resource_components/__init__.py +1 -0
- griptape_nodes/retained_mode/managers/resource_components/capability_field.py +41 -0
- griptape_nodes/retained_mode/managers/resource_components/comparator.py +18 -0
- griptape_nodes/retained_mode/managers/resource_components/resource_instance.py +236 -0
- griptape_nodes/retained_mode/managers/resource_components/resource_type.py +79 -0
- griptape_nodes/retained_mode/managers/resource_manager.py +306 -0
- griptape_nodes/retained_mode/managers/resource_types/__init__.py +1 -0
- griptape_nodes/retained_mode/managers/resource_types/cpu_resource.py +108 -0
- griptape_nodes/retained_mode/managers/resource_types/os_resource.py +87 -0
- griptape_nodes/retained_mode/managers/settings.py +45 -0
- griptape_nodes/retained_mode/managers/sync_manager.py +10 -3
- griptape_nodes/retained_mode/managers/workflow_manager.py +447 -263
- griptape_nodes/traits/multi_options.py +5 -1
- griptape_nodes/traits/options.py +10 -2
- {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/METADATA +2 -2
- {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/RECORD +60 -37
- {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/WHEEL +1 -1
- {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import TYPE_CHECKING, Literal
|
|
3
4
|
|
|
4
|
-
from griptape_nodes.node_library.workflow_registry import WorkflowMetadata
|
|
5
|
+
from griptape_nodes.node_library.workflow_registry import WorkflowMetadata, WorkflowShape
|
|
5
6
|
from griptape_nodes.retained_mode.events.base_events import (
|
|
6
7
|
RequestPayload,
|
|
7
8
|
ResultPayloadFailure,
|
|
@@ -11,6 +12,9 @@ from griptape_nodes.retained_mode.events.base_events import (
|
|
|
11
12
|
)
|
|
12
13
|
from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
|
|
13
14
|
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from griptape_nodes.retained_mode.events.flow_events import SerializedFlowCommands
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
@dataclass
|
|
16
20
|
@PayloadRegistry.register
|
|
@@ -632,3 +636,54 @@ class RegisterWorkflowsFromConfigResultSuccess(WorkflowNotAlteredMixin, ResultPa
|
|
|
632
636
|
@PayloadRegistry.register
|
|
633
637
|
class RegisterWorkflowsFromConfigResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
|
|
634
638
|
"""Workflow registration from configuration failed. Common causes: configuration not found, invalid paths, registration errors."""
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
@dataclass
|
|
642
|
+
@PayloadRegistry.register
|
|
643
|
+
class SaveWorkflowFileFromSerializedFlowRequest(RequestPayload):
|
|
644
|
+
"""Save a workflow file from serialized flow commands without registry overhead.
|
|
645
|
+
|
|
646
|
+
Use when: Creating workflow files from user-supplied subsets of existing workflows,
|
|
647
|
+
exporting partial workflows, creating standalone workflow files without registration.
|
|
648
|
+
|
|
649
|
+
Args:
|
|
650
|
+
serialized_flow_commands: The serialized commands representing the workflow structure
|
|
651
|
+
file_name: Name for the workflow file (without .py extension)
|
|
652
|
+
creation_date: Optional creation date for the workflow metadata (defaults to current time if not provided)
|
|
653
|
+
image_path: Optional path to workflow image/thumbnail
|
|
654
|
+
execution_flow_name: Optional flow name to use for execution code (defaults to file_name if not provided)
|
|
655
|
+
branched_from: Optional branched from information to preserve workflow lineage
|
|
656
|
+
workflow_shape: Optional workflow shape defining inputs and outputs for external callers
|
|
657
|
+
file_path: Optional specific file path to use (defaults to workspace path if not provided)
|
|
658
|
+
|
|
659
|
+
Results: SaveWorkflowFileFromSerializedFlowResultSuccess (with file path) | SaveWorkflowFileFromSerializedFlowResultFailure (save error)
|
|
660
|
+
"""
|
|
661
|
+
|
|
662
|
+
serialized_flow_commands: "SerializedFlowCommands"
|
|
663
|
+
file_name: str
|
|
664
|
+
file_path: str | None = None
|
|
665
|
+
creation_date: datetime | None = None
|
|
666
|
+
image_path: str | None = None
|
|
667
|
+
execution_flow_name: str | None = None
|
|
668
|
+
branched_from: str | None = None
|
|
669
|
+
workflow_shape: WorkflowShape | None = None
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
@dataclass
|
|
673
|
+
@PayloadRegistry.register
|
|
674
|
+
class SaveWorkflowFileFromSerializedFlowResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
|
|
675
|
+
"""Workflow file saved successfully from serialized flow commands.
|
|
676
|
+
|
|
677
|
+
Args:
|
|
678
|
+
file_path: Path where the workflow file was written
|
|
679
|
+
workflow_metadata: The metadata that was generated for the workflow
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
file_path: str
|
|
683
|
+
workflow_metadata: WorkflowMetadata
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
@dataclass
|
|
687
|
+
@PayloadRegistry.register
|
|
688
|
+
class SaveWorkflowFileFromSerializedFlowResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
|
|
689
|
+
"""Workflow file save failed. Common causes: file system error, permission denied, invalid serialized commands."""
|
|
@@ -62,6 +62,7 @@ if TYPE_CHECKING:
|
|
|
62
62
|
from griptape_nodes.retained_mode.managers.event_manager import EventManager
|
|
63
63
|
from griptape_nodes.retained_mode.managers.flow_manager import FlowManager
|
|
64
64
|
from griptape_nodes.retained_mode.managers.library_manager import LibraryManager
|
|
65
|
+
from griptape_nodes.retained_mode.managers.mcp_manager import MCPManager
|
|
65
66
|
from griptape_nodes.retained_mode.managers.model_manager import ModelManager
|
|
66
67
|
from griptape_nodes.retained_mode.managers.node_manager import NodeManager
|
|
67
68
|
from griptape_nodes.retained_mode.managers.object_manager import ObjectManager
|
|
@@ -69,6 +70,7 @@ if TYPE_CHECKING:
|
|
|
69
70
|
OperationDepthManager,
|
|
70
71
|
)
|
|
71
72
|
from griptape_nodes.retained_mode.managers.os_manager import OSManager
|
|
73
|
+
from griptape_nodes.retained_mode.managers.resource_manager import ResourceManager
|
|
72
74
|
from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
|
|
73
75
|
from griptape_nodes.retained_mode.managers.session_manager import SessionManager
|
|
74
76
|
from griptape_nodes.retained_mode.managers.static_files_manager import (
|
|
@@ -149,9 +151,11 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
149
151
|
_version_compatibility_manager: VersionCompatibilityManager
|
|
150
152
|
_session_manager: SessionManager
|
|
151
153
|
_engine_identity_manager: EngineIdentityManager
|
|
154
|
+
_mcp_manager: MCPManager
|
|
155
|
+
_resource_manager: ResourceManager
|
|
152
156
|
_sync_manager: SyncManager
|
|
153
157
|
|
|
154
|
-
def __init__(self) -> None:
|
|
158
|
+
def __init__(self) -> None: # noqa: PLR0915
|
|
155
159
|
from griptape_nodes.retained_mode.managers.agent_manager import AgentManager
|
|
156
160
|
from griptape_nodes.retained_mode.managers.arbitrary_code_exec_manager import (
|
|
157
161
|
ArbitraryCodeExecManager,
|
|
@@ -162,6 +166,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
162
166
|
from griptape_nodes.retained_mode.managers.event_manager import EventManager
|
|
163
167
|
from griptape_nodes.retained_mode.managers.flow_manager import FlowManager
|
|
164
168
|
from griptape_nodes.retained_mode.managers.library_manager import LibraryManager
|
|
169
|
+
from griptape_nodes.retained_mode.managers.mcp_manager import MCPManager
|
|
165
170
|
from griptape_nodes.retained_mode.managers.model_manager import ModelManager
|
|
166
171
|
from griptape_nodes.retained_mode.managers.node_manager import NodeManager
|
|
167
172
|
from griptape_nodes.retained_mode.managers.object_manager import ObjectManager
|
|
@@ -169,6 +174,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
169
174
|
OperationDepthManager,
|
|
170
175
|
)
|
|
171
176
|
from griptape_nodes.retained_mode.managers.os_manager import OSManager
|
|
177
|
+
from griptape_nodes.retained_mode.managers.resource_manager import ResourceManager
|
|
172
178
|
from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
|
|
173
179
|
from griptape_nodes.retained_mode.managers.session_manager import SessionManager
|
|
174
180
|
from griptape_nodes.retained_mode.managers.static_files_manager import (
|
|
@@ -188,6 +194,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
188
194
|
# Initialize only if our managers haven't been created yet
|
|
189
195
|
if not hasattr(self, "_event_manager"):
|
|
190
196
|
self._event_manager = EventManager()
|
|
197
|
+
self._resource_manager = ResourceManager(self._event_manager)
|
|
191
198
|
self._config_manager = ConfigManager(self._event_manager)
|
|
192
199
|
self._os_manager = OSManager(self._event_manager)
|
|
193
200
|
self._secrets_manager = SecretsManager(self._config_manager, self._event_manager)
|
|
@@ -208,6 +215,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
208
215
|
self._version_compatibility_manager = VersionCompatibilityManager(self._event_manager)
|
|
209
216
|
self._session_manager = SessionManager(self._event_manager)
|
|
210
217
|
self._engine_identity_manager = EngineIdentityManager(self._event_manager)
|
|
218
|
+
self._mcp_manager = MCPManager(self._event_manager, self._config_manager)
|
|
211
219
|
self._sync_manager = SyncManager(self._event_manager, self._config_manager)
|
|
212
220
|
|
|
213
221
|
# Assign handlers now that these are created.
|
|
@@ -370,10 +378,18 @@ class GriptapeNodes(metaclass=SingletonMeta):
|
|
|
370
378
|
def SessionManager(cls) -> SessionManager:
|
|
371
379
|
return GriptapeNodes.get_instance()._session_manager
|
|
372
380
|
|
|
381
|
+
@classmethod
|
|
382
|
+
def MCPManager(cls) -> MCPManager:
|
|
383
|
+
return GriptapeNodes.get_instance()._mcp_manager
|
|
384
|
+
|
|
373
385
|
@classmethod
|
|
374
386
|
def EngineIdentityManager(cls) -> EngineIdentityManager:
|
|
375
387
|
return GriptapeNodes.get_instance()._engine_identity_manager
|
|
376
388
|
|
|
389
|
+
@classmethod
|
|
390
|
+
def ResourceManager(cls) -> ResourceManager:
|
|
391
|
+
return GriptapeNodes.get_instance()._resource_manager
|
|
392
|
+
|
|
377
393
|
@classmethod
|
|
378
394
|
def SyncManager(cls) -> SyncManager:
|
|
379
395
|
return GriptapeNodes.get_instance()._sync_manager
|
|
@@ -4,7 +4,7 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import threading
|
|
6
6
|
import uuid
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
7
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
8
8
|
|
|
9
9
|
from attrs import define, field
|
|
10
10
|
from griptape.artifacts import ErrorArtifact, ImageUrlArtifact, JsonArtifact
|
|
@@ -40,6 +40,10 @@ from griptape_nodes.retained_mode.events.agent_events import (
|
|
|
40
40
|
)
|
|
41
41
|
from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
|
|
42
42
|
from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, ExecutionGriptapeNodeEvent, ResultPayload
|
|
43
|
+
from griptape_nodes.retained_mode.events.mcp_events import (
|
|
44
|
+
GetEnabledMCPServersRequest,
|
|
45
|
+
GetEnabledMCPServersResultSuccess,
|
|
46
|
+
)
|
|
43
47
|
from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
|
|
44
48
|
from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
|
|
45
49
|
from griptape_nodes.retained_mode.managers.event_manager import EventManager
|
|
@@ -91,6 +95,14 @@ class NodesPromptImageGenerationTool(BaseImageGenerationTool):
|
|
|
91
95
|
|
|
92
96
|
|
|
93
97
|
class AgentManager:
|
|
98
|
+
# Field mappings for each transport type
|
|
99
|
+
TRANSPORT_FIELD_MAPPINGS: ClassVar[dict[str, list[str]]] = {
|
|
100
|
+
"stdio": ["command", "args", "env", "cwd", "encoding", "encoding_error_handler"],
|
|
101
|
+
"sse": ["url", "headers", "timeout", "sse_read_timeout"],
|
|
102
|
+
"streamable_http": ["url", "headers", "timeout", "sse_read_timeout", "terminate_on_close"],
|
|
103
|
+
"websocket": ["url"],
|
|
104
|
+
}
|
|
105
|
+
|
|
94
106
|
def __init__(self, static_files_manager: StaticFilesManager, event_manager: EventManager | None = None) -> None:
|
|
95
107
|
self.conversation_memory = ConversationMemory()
|
|
96
108
|
self.prompt_driver = None
|
|
@@ -135,7 +147,53 @@ class AgentManager:
|
|
|
135
147
|
"transport": "streamable_http",
|
|
136
148
|
"url": f"http://localhost:{GTN_MCP_SERVER_PORT}/mcp/",
|
|
137
149
|
}
|
|
138
|
-
return MCPTool(connection=connection)
|
|
150
|
+
return MCPTool(connection=connection, name="mcpGriptapeNodes")
|
|
151
|
+
|
|
152
|
+
def _create_additional_mcp_tools(self, server_names: list[str]) -> list[MCPTool]:
|
|
153
|
+
"""Create MCP tools for additional servers specified in the request."""
|
|
154
|
+
additional_tools = []
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
app = GriptapeNodes()
|
|
158
|
+
|
|
159
|
+
enabled_request = GetEnabledMCPServersRequest()
|
|
160
|
+
enabled_result = app.handle_request(enabled_request)
|
|
161
|
+
|
|
162
|
+
if not isinstance(enabled_result, GetEnabledMCPServersResultSuccess):
|
|
163
|
+
msg = f"Failed to get enabled MCP servers for additional tools: {enabled_result}. Agent will continue with default MCP tool only."
|
|
164
|
+
logger.warning(msg)
|
|
165
|
+
return additional_tools
|
|
166
|
+
|
|
167
|
+
for server_name in server_names:
|
|
168
|
+
if server_name in enabled_result.servers:
|
|
169
|
+
server_config = enabled_result.servers[server_name]
|
|
170
|
+
connection = self._create_connection_from_mcp_config(server_config) # type: ignore[arg-type]
|
|
171
|
+
tool = MCPTool(connection=connection, name=f"mcp{server_name.title()}") # type: ignore[arg-type]
|
|
172
|
+
additional_tools.append(tool)
|
|
173
|
+
else:
|
|
174
|
+
msg = f"Additional MCP server '{server_name}' not found or not enabled"
|
|
175
|
+
logger.warning(msg)
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
msg = f"Failed to create additional MCP tools: {e}"
|
|
179
|
+
logger.error(msg)
|
|
180
|
+
|
|
181
|
+
return additional_tools
|
|
182
|
+
|
|
183
|
+
def _create_connection_from_mcp_config(self, server_config: dict) -> dict:
|
|
184
|
+
"""Create connection dictionary from MCP server configuration."""
|
|
185
|
+
transport = server_config.get("transport", "stdio")
|
|
186
|
+
|
|
187
|
+
# Start with transport
|
|
188
|
+
connection = {"transport": transport}
|
|
189
|
+
|
|
190
|
+
# Map relevant fields based on transport type
|
|
191
|
+
fields_to_map = self.TRANSPORT_FIELD_MAPPINGS.get(transport, self.TRANSPORT_FIELD_MAPPINGS["stdio"])
|
|
192
|
+
for field_name in fields_to_map:
|
|
193
|
+
if field_name in server_config and server_config[field_name] is not None:
|
|
194
|
+
connection[field_name] = server_config[field_name]
|
|
195
|
+
|
|
196
|
+
return connection
|
|
139
197
|
|
|
140
198
|
async def on_handle_run_agent_request(self, request: RunAgentRequest) -> ResultPayload:
|
|
141
199
|
if self.prompt_driver is None:
|
|
@@ -147,7 +205,7 @@ class AgentManager:
|
|
|
147
205
|
await asyncio.to_thread(self._on_handle_run_agent_request, request)
|
|
148
206
|
return RunAgentResultStarted(result_details="Agent execution started successfully.")
|
|
149
207
|
|
|
150
|
-
def _create_agent(self) -> Agent:
|
|
208
|
+
def _create_agent(self, additional_mcp_servers: list[str] | None = None) -> Agent:
|
|
151
209
|
output_schema = Schema(
|
|
152
210
|
{
|
|
153
211
|
"generated_image_urls": [str],
|
|
@@ -161,6 +219,11 @@ class AgentManager:
|
|
|
161
219
|
if self.mcp_tool is not None:
|
|
162
220
|
tools.append(self.mcp_tool)
|
|
163
221
|
|
|
222
|
+
# Add additional MCP servers if specified
|
|
223
|
+
if additional_mcp_servers:
|
|
224
|
+
additional_tools = self._create_additional_mcp_tools(additional_mcp_servers)
|
|
225
|
+
tools.extend(additional_tools)
|
|
226
|
+
|
|
164
227
|
return Agent(
|
|
165
228
|
prompt_driver=self.prompt_driver,
|
|
166
229
|
conversation_memory=self.conversation_memory,
|
|
@@ -185,7 +248,7 @@ class AgentManager:
|
|
|
185
248
|
for url_artifact in request.url_artifacts
|
|
186
249
|
if url_artifact["type"] == "ImageUrlArtifact"
|
|
187
250
|
]
|
|
188
|
-
agent = self._create_agent()
|
|
251
|
+
agent = self._create_agent(additional_mcp_servers=request.additional_mcp_servers)
|
|
189
252
|
*events, last_event = agent.run_stream([request.input, *artifacts])
|
|
190
253
|
full_result = ""
|
|
191
254
|
last_conversation_output = ""
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import inspect
|
|
5
|
+
import threading
|
|
5
6
|
from collections import defaultdict
|
|
6
7
|
from dataclasses import fields
|
|
7
8
|
from typing import TYPE_CHECKING, Any, cast
|
|
@@ -43,6 +44,10 @@ class EventManager:
|
|
|
43
44
|
self._flush_in_queue: bool = False
|
|
44
45
|
# Event queue for publishing events
|
|
45
46
|
self._event_queue: asyncio.Queue | None = None
|
|
47
|
+
# Keep track of which thread the event loop runs on
|
|
48
|
+
self._loop_thread_id: int | None = None
|
|
49
|
+
# Keep a reference to the event loop for thread-safe operations
|
|
50
|
+
self._event_loop: asyncio.AbstractEventLoop | None = None
|
|
46
51
|
|
|
47
52
|
@property
|
|
48
53
|
def event_queue(self) -> asyncio.Queue:
|
|
@@ -62,23 +67,48 @@ class EventManager:
|
|
|
62
67
|
"""
|
|
63
68
|
if queue is not None:
|
|
64
69
|
self._event_queue = queue
|
|
70
|
+
# Track which thread the event loop is running on and store loop reference
|
|
71
|
+
try:
|
|
72
|
+
self._event_loop = asyncio.get_running_loop()
|
|
73
|
+
self._loop_thread_id = threading.get_ident()
|
|
74
|
+
except RuntimeError:
|
|
75
|
+
self._event_loop = None
|
|
76
|
+
self._loop_thread_id = None
|
|
65
77
|
else:
|
|
66
78
|
try:
|
|
67
79
|
self._event_queue = asyncio.Queue()
|
|
80
|
+
self._event_loop = asyncio.get_running_loop()
|
|
81
|
+
self._loop_thread_id = threading.get_ident()
|
|
68
82
|
except RuntimeError:
|
|
69
83
|
# Defer queue creation until we're in an event loop
|
|
70
84
|
self._event_queue = None
|
|
85
|
+
self._event_loop = None
|
|
86
|
+
self._loop_thread_id = None
|
|
71
87
|
|
|
72
88
|
def put_event(self, event: Any) -> None:
|
|
73
89
|
"""Put event into async queue from sync context (non-blocking).
|
|
74
90
|
|
|
91
|
+
Automatically detects if we're in a different thread and uses thread-safe operations.
|
|
92
|
+
|
|
75
93
|
Args:
|
|
76
94
|
event: The event to publish to the queue
|
|
77
95
|
"""
|
|
78
96
|
if self._event_queue is None:
|
|
79
97
|
return
|
|
80
98
|
|
|
81
|
-
|
|
99
|
+
# Check if we need thread-safe operation
|
|
100
|
+
current_thread_id = threading.get_ident()
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
self._loop_thread_id is not None
|
|
104
|
+
and current_thread_id != self._loop_thread_id
|
|
105
|
+
and self._event_loop is not None
|
|
106
|
+
):
|
|
107
|
+
# We're in a different thread from the event loop, use thread-safe method
|
|
108
|
+
self._event_loop.call_soon_threadsafe(self._event_queue.put_nowait, event)
|
|
109
|
+
else:
|
|
110
|
+
# We're on the same thread as the event loop or no loop thread tracked, use direct method
|
|
111
|
+
self._event_queue.put_nowait(event)
|
|
82
112
|
|
|
83
113
|
async def aput_event(self, event: Any) -> None:
|
|
84
114
|
"""Put event into async queue from async context.
|
|
@@ -91,18 +121,6 @@ class EventManager:
|
|
|
91
121
|
|
|
92
122
|
await self._event_queue.put(event)
|
|
93
123
|
|
|
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
|
-
|
|
106
124
|
def assign_manager_to_request_type(
|
|
107
125
|
self,
|
|
108
126
|
request_type: type[RP],
|