kailash 0.6.6__py3-none-any.whl → 0.8.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.
- kailash/__init__.py +35 -5
- kailash/access_control.py +64 -46
- kailash/adapters/__init__.py +5 -0
- kailash/adapters/mcp_platform_adapter.py +273 -0
- kailash/api/workflow_api.py +34 -3
- kailash/channels/__init__.py +21 -0
- kailash/channels/api_channel.py +409 -0
- kailash/channels/base.py +271 -0
- kailash/channels/cli_channel.py +661 -0
- kailash/channels/event_router.py +496 -0
- kailash/channels/mcp_channel.py +648 -0
- kailash/channels/session.py +423 -0
- kailash/mcp_server/discovery.py +57 -18
- kailash/middleware/communication/api_gateway.py +23 -3
- kailash/middleware/communication/realtime.py +83 -0
- kailash/middleware/core/agent_ui.py +1 -1
- kailash/middleware/gateway/storage_backends.py +393 -0
- kailash/middleware/mcp/enhanced_server.py +22 -16
- kailash/nexus/__init__.py +21 -0
- kailash/nexus/cli/__init__.py +5 -0
- kailash/nexus/cli/__main__.py +6 -0
- kailash/nexus/cli/main.py +176 -0
- kailash/nexus/factory.py +413 -0
- kailash/nexus/gateway.py +545 -0
- kailash/nodes/__init__.py +8 -5
- kailash/nodes/ai/iterative_llm_agent.py +988 -17
- kailash/nodes/ai/llm_agent.py +29 -9
- kailash/nodes/api/__init__.py +2 -2
- kailash/nodes/api/monitoring.py +1 -1
- kailash/nodes/base.py +29 -5
- kailash/nodes/base_async.py +54 -14
- kailash/nodes/code/async_python.py +1 -1
- kailash/nodes/code/python.py +50 -6
- kailash/nodes/data/async_sql.py +90 -0
- kailash/nodes/data/bulk_operations.py +939 -0
- kailash/nodes/data/query_builder.py +373 -0
- kailash/nodes/data/query_cache.py +512 -0
- kailash/nodes/monitoring/__init__.py +10 -0
- kailash/nodes/monitoring/deadlock_detector.py +964 -0
- kailash/nodes/monitoring/performance_anomaly.py +1078 -0
- kailash/nodes/monitoring/race_condition_detector.py +1151 -0
- kailash/nodes/monitoring/transaction_metrics.py +790 -0
- kailash/nodes/monitoring/transaction_monitor.py +931 -0
- kailash/nodes/security/behavior_analysis.py +414 -0
- kailash/nodes/system/__init__.py +17 -0
- kailash/nodes/system/command_parser.py +820 -0
- kailash/nodes/transaction/__init__.py +48 -0
- kailash/nodes/transaction/distributed_transaction_manager.py +983 -0
- kailash/nodes/transaction/saga_coordinator.py +652 -0
- kailash/nodes/transaction/saga_state_storage.py +411 -0
- kailash/nodes/transaction/saga_step.py +467 -0
- kailash/nodes/transaction/transaction_context.py +756 -0
- kailash/nodes/transaction/two_phase_commit.py +978 -0
- kailash/nodes/transform/processors.py +17 -1
- kailash/nodes/validation/__init__.py +21 -0
- kailash/nodes/validation/test_executor.py +532 -0
- kailash/nodes/validation/validation_nodes.py +447 -0
- kailash/resources/factory.py +1 -1
- kailash/runtime/access_controlled.py +9 -7
- kailash/runtime/async_local.py +84 -21
- kailash/runtime/local.py +21 -2
- kailash/runtime/parameter_injector.py +187 -31
- kailash/runtime/runner.py +6 -4
- kailash/runtime/testing.py +1 -1
- kailash/security.py +22 -3
- kailash/servers/__init__.py +32 -0
- kailash/servers/durable_workflow_server.py +430 -0
- kailash/servers/enterprise_workflow_server.py +522 -0
- kailash/servers/gateway.py +183 -0
- kailash/servers/workflow_server.py +293 -0
- kailash/utils/data_validation.py +192 -0
- kailash/workflow/builder.py +382 -15
- kailash/workflow/cyclic_runner.py +102 -10
- kailash/workflow/validation.py +144 -8
- kailash/workflow/visualization.py +99 -27
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/METADATA +3 -2
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/RECORD +81 -40
- kailash/workflow/builder_improvements.py +0 -207
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/WHEEL +0 -0
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/top_level.txt +0 -0
kailash/nexus/gateway.py
ADDED
@@ -0,0 +1,545 @@
|
|
1
|
+
"""Nexus Gateway - Main orchestration hub for multi-channel communication."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import logging
|
5
|
+
import signal
|
6
|
+
import sys
|
7
|
+
from dataclasses import dataclass, field
|
8
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
9
|
+
|
10
|
+
from ..channels.api_channel import APIChannel
|
11
|
+
from ..channels.base import Channel, ChannelConfig, ChannelStatus, ChannelType
|
12
|
+
from ..channels.cli_channel import CLIChannel
|
13
|
+
from ..channels.event_router import EventRoute, EventRouter, RoutingRule
|
14
|
+
from ..channels.mcp_channel import MCPChannel
|
15
|
+
from ..channels.session import SessionManager
|
16
|
+
from ..workflow import Workflow
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
@dataclass
|
22
|
+
class NexusConfig:
|
23
|
+
"""Configuration for Nexus Gateway."""
|
24
|
+
|
25
|
+
# Basic settings
|
26
|
+
name: str = "kailash-nexus"
|
27
|
+
description: str = "Multi-channel workflow orchestration gateway"
|
28
|
+
version: str = "1.0.0"
|
29
|
+
|
30
|
+
# Channel configuration
|
31
|
+
enable_api: bool = True
|
32
|
+
enable_cli: bool = True
|
33
|
+
enable_mcp: bool = True
|
34
|
+
|
35
|
+
# API channel settings
|
36
|
+
api_host: str = "localhost"
|
37
|
+
api_port: int = 8000
|
38
|
+
api_cors_origins: List[str] = field(default_factory=lambda: ["*"])
|
39
|
+
|
40
|
+
# CLI channel settings
|
41
|
+
cli_interactive: bool = False
|
42
|
+
cli_prompt_template: str = "nexus> "
|
43
|
+
|
44
|
+
# MCP channel settings
|
45
|
+
mcp_host: str = "localhost"
|
46
|
+
mcp_port: int = 3001
|
47
|
+
mcp_server_name: str = "kailash-nexus-mcp"
|
48
|
+
|
49
|
+
# Session management
|
50
|
+
session_timeout: int = 3600 # 1 hour
|
51
|
+
session_cleanup_interval: int = 300 # 5 minutes
|
52
|
+
|
53
|
+
# Event routing
|
54
|
+
enable_event_routing: bool = True
|
55
|
+
event_queue_size: int = 10000
|
56
|
+
|
57
|
+
# Advanced settings
|
58
|
+
enable_health_monitoring: bool = True
|
59
|
+
health_check_interval: int = 30
|
60
|
+
graceful_shutdown_timeout: int = 30
|
61
|
+
|
62
|
+
|
63
|
+
class NexusGateway:
|
64
|
+
"""Main orchestration hub for the Kailash Nexus framework.
|
65
|
+
|
66
|
+
The Nexus Gateway provides a unified interface for managing multiple
|
67
|
+
communication channels (API, CLI, MCP) with shared session management
|
68
|
+
and cross-channel event routing.
|
69
|
+
"""
|
70
|
+
|
71
|
+
def __init__(self, config: Optional[NexusConfig] = None):
|
72
|
+
"""Initialize Nexus Gateway.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
config: Optional configuration, uses defaults if not provided
|
76
|
+
"""
|
77
|
+
self.config = config or NexusConfig()
|
78
|
+
|
79
|
+
# Core components
|
80
|
+
self.session_manager = SessionManager(
|
81
|
+
default_timeout=self.config.session_timeout,
|
82
|
+
cleanup_interval=self.config.session_cleanup_interval,
|
83
|
+
)
|
84
|
+
self.event_router = EventRouter(session_manager=self.session_manager)
|
85
|
+
|
86
|
+
# Channels
|
87
|
+
self._channels: Dict[str, Channel] = {}
|
88
|
+
self._workflows: Dict[str, Workflow] = {}
|
89
|
+
|
90
|
+
# Runtime state
|
91
|
+
self._running = False
|
92
|
+
self._startup_tasks: List[asyncio.Task] = []
|
93
|
+
self._health_task: Optional[asyncio.Task] = None
|
94
|
+
self._shutdown_event = asyncio.Event()
|
95
|
+
|
96
|
+
# Initialize channels based on configuration
|
97
|
+
self._initialize_channels()
|
98
|
+
|
99
|
+
logger.info(f"Nexus Gateway initialized: {self.config.name}")
|
100
|
+
|
101
|
+
def _initialize_channels(self) -> None:
|
102
|
+
"""Initialize channels based on configuration."""
|
103
|
+
|
104
|
+
# API Channel
|
105
|
+
if self.config.enable_api:
|
106
|
+
api_config = ChannelConfig(
|
107
|
+
name="api",
|
108
|
+
channel_type=ChannelType.API,
|
109
|
+
host=self.config.api_host,
|
110
|
+
port=self.config.api_port,
|
111
|
+
enable_event_routing=self.config.enable_event_routing,
|
112
|
+
extra_config={
|
113
|
+
"title": f"{self.config.name} API",
|
114
|
+
"description": f"API interface for {self.config.description}",
|
115
|
+
"cors_origins": self.config.api_cors_origins,
|
116
|
+
"enable_durability": True,
|
117
|
+
"enable_resource_management": True,
|
118
|
+
"enable_async_execution": True,
|
119
|
+
"enable_health_checks": True,
|
120
|
+
},
|
121
|
+
)
|
122
|
+
self._channels["api"] = APIChannel(api_config)
|
123
|
+
|
124
|
+
# CLI Channel
|
125
|
+
if self.config.enable_cli:
|
126
|
+
cli_config = ChannelConfig(
|
127
|
+
name="cli",
|
128
|
+
channel_type=ChannelType.CLI,
|
129
|
+
host=self.config.api_host, # CLI doesn't use network host
|
130
|
+
enable_event_routing=self.config.enable_event_routing,
|
131
|
+
extra_config={
|
132
|
+
"interactive_mode": self.config.cli_interactive,
|
133
|
+
"prompt_template": self.config.cli_prompt_template,
|
134
|
+
},
|
135
|
+
)
|
136
|
+
self._channels["cli"] = CLIChannel(cli_config)
|
137
|
+
|
138
|
+
# MCP Channel
|
139
|
+
if self.config.enable_mcp:
|
140
|
+
mcp_config = ChannelConfig(
|
141
|
+
name="mcp",
|
142
|
+
channel_type=ChannelType.MCP,
|
143
|
+
host=self.config.mcp_host,
|
144
|
+
port=self.config.mcp_port,
|
145
|
+
enable_event_routing=self.config.enable_event_routing,
|
146
|
+
extra_config={
|
147
|
+
"server_name": self.config.mcp_server_name,
|
148
|
+
"description": f"MCP interface for {self.config.description}",
|
149
|
+
},
|
150
|
+
)
|
151
|
+
self._channels["mcp"] = MCPChannel(mcp_config)
|
152
|
+
|
153
|
+
async def start(self) -> None:
|
154
|
+
"""Start the Nexus Gateway and all enabled channels."""
|
155
|
+
if self._running:
|
156
|
+
logger.warning("Nexus Gateway is already running")
|
157
|
+
return
|
158
|
+
|
159
|
+
try:
|
160
|
+
logger.info(f"Starting Nexus Gateway: {self.config.name}")
|
161
|
+
|
162
|
+
# Start session manager
|
163
|
+
await self.session_manager.start()
|
164
|
+
|
165
|
+
# Start event router
|
166
|
+
await self.event_router.start()
|
167
|
+
|
168
|
+
# Register channels with event router
|
169
|
+
for channel in self._channels.values():
|
170
|
+
self.event_router.register_channel(channel)
|
171
|
+
|
172
|
+
# Start all channels
|
173
|
+
channel_tasks = []
|
174
|
+
for channel_name, channel in self._channels.items():
|
175
|
+
logger.info(f"Starting {channel_name} channel...")
|
176
|
+
task = asyncio.create_task(channel.start())
|
177
|
+
task.set_name(f"start_{channel_name}")
|
178
|
+
channel_tasks.append(task)
|
179
|
+
|
180
|
+
# Wait for all channels to start
|
181
|
+
await asyncio.gather(*channel_tasks, return_exceptions=True)
|
182
|
+
|
183
|
+
# Verify channel startup
|
184
|
+
failed_channels = []
|
185
|
+
for channel_name, channel in self._channels.items():
|
186
|
+
if channel.status != ChannelStatus.RUNNING:
|
187
|
+
failed_channels.append(channel_name)
|
188
|
+
logger.error(
|
189
|
+
f"Failed to start {channel_name} channel: {channel.status}"
|
190
|
+
)
|
191
|
+
|
192
|
+
if failed_channels:
|
193
|
+
raise RuntimeError(f"Failed to start channels: {failed_channels}")
|
194
|
+
|
195
|
+
# Start health monitoring if enabled
|
196
|
+
if self.config.enable_health_monitoring:
|
197
|
+
self._health_task = asyncio.create_task(self._health_monitoring_loop())
|
198
|
+
|
199
|
+
# Setup signal handlers for graceful shutdown
|
200
|
+
self._setup_signal_handlers()
|
201
|
+
|
202
|
+
self._running = True
|
203
|
+
|
204
|
+
logger.info(
|
205
|
+
f"Nexus Gateway started successfully with {len(self._channels)} channels"
|
206
|
+
)
|
207
|
+
|
208
|
+
# Register shared workflows with all channels
|
209
|
+
await self._register_workflows()
|
210
|
+
|
211
|
+
except Exception as e:
|
212
|
+
logger.error(f"Failed to start Nexus Gateway: {e}")
|
213
|
+
await self.stop() # Cleanup on failure
|
214
|
+
raise
|
215
|
+
|
216
|
+
async def stop(self) -> None:
|
217
|
+
"""Stop the Nexus Gateway and all channels."""
|
218
|
+
if not self._running:
|
219
|
+
return
|
220
|
+
|
221
|
+
try:
|
222
|
+
logger.info("Stopping Nexus Gateway...")
|
223
|
+
self._running = False
|
224
|
+
self._shutdown_event.set()
|
225
|
+
|
226
|
+
# Stop health monitoring
|
227
|
+
if self._health_task and not self._health_task.done():
|
228
|
+
self._health_task.cancel()
|
229
|
+
try:
|
230
|
+
await self._health_task
|
231
|
+
except asyncio.CancelledError:
|
232
|
+
pass
|
233
|
+
|
234
|
+
# Stop all channels
|
235
|
+
channel_tasks = []
|
236
|
+
for channel_name, channel in self._channels.items():
|
237
|
+
logger.info(f"Stopping {channel_name} channel...")
|
238
|
+
task = asyncio.create_task(channel.stop())
|
239
|
+
task.set_name(f"stop_{channel_name}")
|
240
|
+
channel_tasks.append(task)
|
241
|
+
|
242
|
+
# Wait for channels to stop (with timeout)
|
243
|
+
try:
|
244
|
+
await asyncio.wait_for(
|
245
|
+
asyncio.gather(*channel_tasks, return_exceptions=True),
|
246
|
+
timeout=self.config.graceful_shutdown_timeout,
|
247
|
+
)
|
248
|
+
except asyncio.TimeoutError:
|
249
|
+
logger.warning("Channel shutdown timed out")
|
250
|
+
|
251
|
+
# Stop event router
|
252
|
+
await self.event_router.stop()
|
253
|
+
|
254
|
+
# Stop session manager
|
255
|
+
await self.session_manager.stop()
|
256
|
+
|
257
|
+
logger.info("Nexus Gateway stopped")
|
258
|
+
|
259
|
+
except Exception as e:
|
260
|
+
logger.error(f"Error stopping Nexus Gateway: {e}")
|
261
|
+
|
262
|
+
def register_workflow(
|
263
|
+
self, name: str, workflow: Workflow, channels: Optional[List[str]] = None
|
264
|
+
) -> None:
|
265
|
+
"""Register a workflow with specified channels.
|
266
|
+
|
267
|
+
Args:
|
268
|
+
name: Workflow name
|
269
|
+
workflow: Workflow instance
|
270
|
+
channels: List of channel names to register with (all channels if None)
|
271
|
+
"""
|
272
|
+
self._workflows[name] = workflow
|
273
|
+
|
274
|
+
# Register with specified channels (or all channels if none specified)
|
275
|
+
target_channels = channels or list(self._channels.keys())
|
276
|
+
|
277
|
+
for channel_name in target_channels:
|
278
|
+
if channel_name in self._channels:
|
279
|
+
channel = self._channels[channel_name]
|
280
|
+
|
281
|
+
# Register based on channel type
|
282
|
+
if isinstance(channel, APIChannel):
|
283
|
+
channel.register_workflow(name, workflow)
|
284
|
+
elif isinstance(channel, MCPChannel):
|
285
|
+
channel.register_workflow(name, workflow)
|
286
|
+
# CLI channel uses workflows through routing
|
287
|
+
|
288
|
+
logger.info(f"Registered workflow '{name}' with {channel_name} channel")
|
289
|
+
|
290
|
+
def proxy_workflow(
|
291
|
+
self,
|
292
|
+
name: str,
|
293
|
+
proxy_url: str,
|
294
|
+
channels: Optional[List[str]] = None,
|
295
|
+
health_check: Optional[str] = None,
|
296
|
+
) -> None:
|
297
|
+
"""Register a proxied workflow with specified channels.
|
298
|
+
|
299
|
+
Args:
|
300
|
+
name: Workflow name
|
301
|
+
proxy_url: URL to proxy requests to
|
302
|
+
channels: List of channel names to register with
|
303
|
+
health_check: Optional health check endpoint
|
304
|
+
"""
|
305
|
+
target_channels = channels or [
|
306
|
+
"api"
|
307
|
+
] # Default to API only for proxied workflows
|
308
|
+
|
309
|
+
for channel_name in target_channels:
|
310
|
+
if channel_name in self._channels and isinstance(
|
311
|
+
self._channels[channel_name], APIChannel
|
312
|
+
):
|
313
|
+
channel = self._channels[channel_name]
|
314
|
+
channel.proxy_workflow(name, proxy_url, health_check)
|
315
|
+
logger.info(
|
316
|
+
f"Registered proxied workflow '{name}' with {channel_name} channel"
|
317
|
+
)
|
318
|
+
|
319
|
+
async def _register_workflows(self) -> None:
|
320
|
+
"""Register all stored workflows with appropriate channels."""
|
321
|
+
for name, workflow in self._workflows.items():
|
322
|
+
self.register_workflow(name, workflow)
|
323
|
+
|
324
|
+
def get_channel(self, name: str) -> Optional[Channel]:
|
325
|
+
"""Get a channel by name.
|
326
|
+
|
327
|
+
Args:
|
328
|
+
name: Channel name
|
329
|
+
|
330
|
+
Returns:
|
331
|
+
Channel instance or None if not found
|
332
|
+
"""
|
333
|
+
return self._channels.get(name)
|
334
|
+
|
335
|
+
def list_channels(self) -> Dict[str, Dict[str, Any]]:
|
336
|
+
"""List all channels and their status.
|
337
|
+
|
338
|
+
Returns:
|
339
|
+
Dictionary of channel information
|
340
|
+
"""
|
341
|
+
channels_info = {}
|
342
|
+
|
343
|
+
for name, channel in self._channels.items():
|
344
|
+
channels_info[name] = {
|
345
|
+
"name": name,
|
346
|
+
"type": channel.channel_type.value,
|
347
|
+
"status": channel.status.value,
|
348
|
+
"config": {
|
349
|
+
"host": channel.config.host,
|
350
|
+
"port": channel.config.port,
|
351
|
+
"enabled": channel.config.enabled,
|
352
|
+
},
|
353
|
+
}
|
354
|
+
|
355
|
+
return channels_info
|
356
|
+
|
357
|
+
def list_workflows(self) -> Dict[str, Dict[str, Any]]:
|
358
|
+
"""List all registered workflows.
|
359
|
+
|
360
|
+
Returns:
|
361
|
+
Dictionary of workflow information
|
362
|
+
"""
|
363
|
+
workflows_info = {}
|
364
|
+
|
365
|
+
for name, workflow in self._workflows.items():
|
366
|
+
workflows_info[name] = {"name": name, "available_on": []}
|
367
|
+
|
368
|
+
# Check which channels have this workflow
|
369
|
+
for channel_name, channel in self._channels.items():
|
370
|
+
if (
|
371
|
+
isinstance(channel, APIChannel)
|
372
|
+
and name in channel.workflow_server.workflows
|
373
|
+
):
|
374
|
+
workflows_info[name]["available_on"].append(channel_name)
|
375
|
+
elif (
|
376
|
+
isinstance(channel, MCPChannel)
|
377
|
+
and name in channel._workflow_registry
|
378
|
+
):
|
379
|
+
workflows_info[name]["available_on"].append(channel_name)
|
380
|
+
|
381
|
+
return workflows_info
|
382
|
+
|
383
|
+
async def health_check(self) -> Dict[str, Any]:
|
384
|
+
"""Perform comprehensive health check.
|
385
|
+
|
386
|
+
Returns:
|
387
|
+
Health check results
|
388
|
+
"""
|
389
|
+
overall_healthy = True
|
390
|
+
checks = {}
|
391
|
+
|
392
|
+
# Check session manager
|
393
|
+
try:
|
394
|
+
session_stats = self.session_manager.get_stats()
|
395
|
+
checks["session_manager"] = {
|
396
|
+
"healthy": True,
|
397
|
+
"total_sessions": session_stats["total_sessions"],
|
398
|
+
"active_sessions": session_stats["active_sessions"],
|
399
|
+
}
|
400
|
+
except Exception as e:
|
401
|
+
checks["session_manager"] = {"healthy": False, "error": str(e)}
|
402
|
+
overall_healthy = False
|
403
|
+
|
404
|
+
# Check event router
|
405
|
+
try:
|
406
|
+
router_health = await self.event_router.health_check()
|
407
|
+
checks["event_router"] = router_health
|
408
|
+
if not router_health["healthy"]:
|
409
|
+
overall_healthy = False
|
410
|
+
except Exception as e:
|
411
|
+
checks["event_router"] = {"healthy": False, "error": str(e)}
|
412
|
+
overall_healthy = False
|
413
|
+
|
414
|
+
# Check all channels
|
415
|
+
channel_checks = {}
|
416
|
+
for name, channel in self._channels.items():
|
417
|
+
try:
|
418
|
+
channel_health = await channel.health_check()
|
419
|
+
channel_checks[name] = channel_health
|
420
|
+
if not channel_health["healthy"]:
|
421
|
+
overall_healthy = False
|
422
|
+
except Exception as e:
|
423
|
+
channel_checks[name] = {"healthy": False, "error": str(e)}
|
424
|
+
overall_healthy = False
|
425
|
+
|
426
|
+
checks["channels"] = channel_checks
|
427
|
+
|
428
|
+
return {
|
429
|
+
"healthy": overall_healthy,
|
430
|
+
"nexus_running": self._running,
|
431
|
+
"channels_count": len(self._channels),
|
432
|
+
"workflows_count": len(self._workflows),
|
433
|
+
"checks": checks,
|
434
|
+
"channels": list(self._channels.keys()),
|
435
|
+
"config": {
|
436
|
+
"name": self.config.name,
|
437
|
+
"version": self.config.version,
|
438
|
+
"enable_api": self.config.enable_api,
|
439
|
+
"enable_cli": self.config.enable_cli,
|
440
|
+
"enable_mcp": self.config.enable_mcp,
|
441
|
+
},
|
442
|
+
}
|
443
|
+
|
444
|
+
async def get_stats(self) -> Dict[str, Any]:
|
445
|
+
"""Get comprehensive Nexus Gateway statistics.
|
446
|
+
|
447
|
+
Returns:
|
448
|
+
Statistics dictionary
|
449
|
+
"""
|
450
|
+
stats = {
|
451
|
+
"nexus": {
|
452
|
+
"name": self.config.name,
|
453
|
+
"version": self.config.version,
|
454
|
+
"running": self._running,
|
455
|
+
"channels_enabled": len(self._channels),
|
456
|
+
"workflows_registered": len(self._workflows),
|
457
|
+
},
|
458
|
+
"session_manager": self.session_manager.get_stats(),
|
459
|
+
"event_router": self.event_router.get_stats(),
|
460
|
+
"channels": {},
|
461
|
+
}
|
462
|
+
|
463
|
+
# Get channel-specific stats
|
464
|
+
for name, channel in self._channels.items():
|
465
|
+
try:
|
466
|
+
channel_status = await channel.get_status()
|
467
|
+
stats["channels"][name] = channel_status
|
468
|
+
except Exception as e:
|
469
|
+
stats["channels"][name] = {"error": str(e)}
|
470
|
+
|
471
|
+
return stats
|
472
|
+
|
473
|
+
async def _health_monitoring_loop(self) -> None:
|
474
|
+
"""Background health monitoring loop."""
|
475
|
+
while self._running:
|
476
|
+
try:
|
477
|
+
await asyncio.sleep(self.config.health_check_interval)
|
478
|
+
|
479
|
+
if not self._running:
|
480
|
+
break
|
481
|
+
|
482
|
+
# Perform health check
|
483
|
+
health = await self.health_check()
|
484
|
+
|
485
|
+
if not health["healthy"]:
|
486
|
+
logger.warning("Nexus Gateway health check failed")
|
487
|
+
logger.debug(f"Health check details: {health}")
|
488
|
+
|
489
|
+
# Check for unhealthy channels
|
490
|
+
for channel_name, channel_health in health["checks"][
|
491
|
+
"channels"
|
492
|
+
].items():
|
493
|
+
if not channel_health.get("healthy", False):
|
494
|
+
logger.warning(
|
495
|
+
f"Channel {channel_name} is unhealthy: {channel_health}"
|
496
|
+
)
|
497
|
+
|
498
|
+
except asyncio.CancelledError:
|
499
|
+
break
|
500
|
+
except Exception as e:
|
501
|
+
logger.error(f"Error in health monitoring: {e}")
|
502
|
+
|
503
|
+
def _setup_signal_handlers(self) -> None:
|
504
|
+
"""Setup signal handlers for graceful shutdown."""
|
505
|
+
if sys.platform != "win32":
|
506
|
+
# Unix-style signal handling
|
507
|
+
loop = asyncio.get_event_loop()
|
508
|
+
|
509
|
+
def signal_handler():
|
510
|
+
logger.info("Received shutdown signal")
|
511
|
+
asyncio.create_task(self.stop())
|
512
|
+
|
513
|
+
loop.add_signal_handler(signal.SIGINT, signal_handler)
|
514
|
+
loop.add_signal_handler(signal.SIGTERM, signal_handler)
|
515
|
+
|
516
|
+
async def run_forever(self) -> None:
|
517
|
+
"""Run the Nexus Gateway until shutdown signal."""
|
518
|
+
if not self._running:
|
519
|
+
await self.start()
|
520
|
+
|
521
|
+
try:
|
522
|
+
logger.info("Nexus Gateway is running. Press Ctrl+C to stop.")
|
523
|
+
await self._shutdown_event.wait()
|
524
|
+
except KeyboardInterrupt:
|
525
|
+
logger.info("Received keyboard interrupt")
|
526
|
+
finally:
|
527
|
+
await self.stop()
|
528
|
+
|
529
|
+
def __enter__(self):
|
530
|
+
"""Context manager entry."""
|
531
|
+
return self
|
532
|
+
|
533
|
+
async def __aenter__(self):
|
534
|
+
"""Async context manager entry."""
|
535
|
+
await self.start()
|
536
|
+
return self
|
537
|
+
|
538
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
539
|
+
"""Context manager exit."""
|
540
|
+
# Note: This is synchronous, for async context use __aexit__
|
541
|
+
pass
|
542
|
+
|
543
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
544
|
+
"""Async context manager exit."""
|
545
|
+
await self.stop()
|
kailash/nodes/__init__.py
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
"""Node system for the Kailash SDK."""
|
2
2
|
|
3
|
-
# Import all node modules to ensure registration
|
4
|
-
from kailash.nodes import
|
3
|
+
# Import all node modules to ensure registration - fixed circular import
|
4
|
+
from kailash.nodes.base import Node, NodeParameter, NodeRegistry, register_node
|
5
|
+
from kailash.nodes.base_cycle_aware import CycleAwareNode
|
6
|
+
from kailash.nodes.code import PythonCodeNode
|
7
|
+
|
8
|
+
from . import (
|
5
9
|
ai,
|
6
10
|
alerts,
|
7
11
|
api,
|
@@ -16,11 +20,9 @@ from kailash.nodes import (
|
|
16
20
|
monitoring,
|
17
21
|
security,
|
18
22
|
testing,
|
23
|
+
transaction,
|
19
24
|
transform,
|
20
25
|
)
|
21
|
-
from kailash.nodes.base import Node, NodeParameter, NodeRegistry, register_node
|
22
|
-
from kailash.nodes.base_cycle_aware import CycleAwareNode
|
23
|
-
from kailash.nodes.code import PythonCodeNode
|
24
26
|
|
25
27
|
# Compatibility alias - AsyncNode is now just Node
|
26
28
|
AsyncNode = Node
|
@@ -48,5 +50,6 @@ __all__ = [
|
|
48
50
|
"monitoring",
|
49
51
|
"security",
|
50
52
|
"testing",
|
53
|
+
"transaction",
|
51
54
|
"transform",
|
52
55
|
]
|