kailash 0.6.5__py3-none-any.whl → 0.7.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.
Files changed (64) hide show
  1. kailash/__init__.py +35 -4
  2. kailash/adapters/__init__.py +5 -0
  3. kailash/adapters/mcp_platform_adapter.py +273 -0
  4. kailash/channels/__init__.py +21 -0
  5. kailash/channels/api_channel.py +409 -0
  6. kailash/channels/base.py +271 -0
  7. kailash/channels/cli_channel.py +661 -0
  8. kailash/channels/event_router.py +496 -0
  9. kailash/channels/mcp_channel.py +648 -0
  10. kailash/channels/session.py +423 -0
  11. kailash/mcp_server/discovery.py +1 -1
  12. kailash/middleware/core/agent_ui.py +5 -0
  13. kailash/middleware/mcp/enhanced_server.py +22 -16
  14. kailash/nexus/__init__.py +21 -0
  15. kailash/nexus/factory.py +413 -0
  16. kailash/nexus/gateway.py +545 -0
  17. kailash/nodes/__init__.py +2 -0
  18. kailash/nodes/ai/iterative_llm_agent.py +988 -17
  19. kailash/nodes/ai/llm_agent.py +29 -9
  20. kailash/nodes/api/__init__.py +2 -2
  21. kailash/nodes/api/monitoring.py +1 -1
  22. kailash/nodes/base_async.py +54 -14
  23. kailash/nodes/code/async_python.py +1 -1
  24. kailash/nodes/data/bulk_operations.py +939 -0
  25. kailash/nodes/data/query_builder.py +373 -0
  26. kailash/nodes/data/query_cache.py +512 -0
  27. kailash/nodes/monitoring/__init__.py +10 -0
  28. kailash/nodes/monitoring/deadlock_detector.py +964 -0
  29. kailash/nodes/monitoring/performance_anomaly.py +1078 -0
  30. kailash/nodes/monitoring/race_condition_detector.py +1151 -0
  31. kailash/nodes/monitoring/transaction_metrics.py +790 -0
  32. kailash/nodes/monitoring/transaction_monitor.py +931 -0
  33. kailash/nodes/system/__init__.py +17 -0
  34. kailash/nodes/system/command_parser.py +820 -0
  35. kailash/nodes/transaction/__init__.py +48 -0
  36. kailash/nodes/transaction/distributed_transaction_manager.py +983 -0
  37. kailash/nodes/transaction/saga_coordinator.py +652 -0
  38. kailash/nodes/transaction/saga_state_storage.py +411 -0
  39. kailash/nodes/transaction/saga_step.py +467 -0
  40. kailash/nodes/transaction/transaction_context.py +756 -0
  41. kailash/nodes/transaction/two_phase_commit.py +978 -0
  42. kailash/nodes/transform/processors.py +17 -1
  43. kailash/nodes/validation/__init__.py +21 -0
  44. kailash/nodes/validation/test_executor.py +532 -0
  45. kailash/nodes/validation/validation_nodes.py +447 -0
  46. kailash/resources/factory.py +1 -1
  47. kailash/runtime/async_local.py +84 -21
  48. kailash/runtime/local.py +21 -2
  49. kailash/runtime/parameter_injector.py +187 -31
  50. kailash/security.py +16 -1
  51. kailash/servers/__init__.py +32 -0
  52. kailash/servers/durable_workflow_server.py +430 -0
  53. kailash/servers/enterprise_workflow_server.py +466 -0
  54. kailash/servers/gateway.py +183 -0
  55. kailash/servers/workflow_server.py +290 -0
  56. kailash/utils/data_validation.py +192 -0
  57. kailash/workflow/builder.py +291 -12
  58. kailash/workflow/validation.py +144 -8
  59. {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/METADATA +1 -1
  60. {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/RECORD +64 -26
  61. {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/WHEEL +0 -0
  62. {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/entry_points.txt +0 -0
  63. {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/licenses/LICENSE +0 -0
  64. {kailash-0.6.5.dist-info → kailash-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,496 @@
1
+ """Cross-channel event router for Nexus framework."""
2
+
3
+ import asyncio
4
+ import fnmatch
5
+ import logging
6
+ import time
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum
9
+ from typing import Any, Callable, Dict, List, Optional, Set, Union
10
+
11
+ from .base import Channel, ChannelEvent
12
+ from .session import CrossChannelSession, SessionManager
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class RoutingRule(Enum):
18
+ """Event routing rule types."""
19
+
20
+ BROADCAST = "broadcast" # Send to all channels
21
+ UNICAST = "unicast" # Send to specific channel
22
+ MULTICAST = "multicast" # Send to selected channels
23
+ SESSION = "session" # Route based on session
24
+ PATTERN = "pattern" # Route based on pattern matching
25
+
26
+
27
+ @dataclass
28
+ class EventRoute:
29
+ """Defines an event routing rule."""
30
+
31
+ rule_type: RoutingRule
32
+ source_patterns: List[str] = field(default_factory=list)
33
+ target_channels: List[str] = field(default_factory=list)
34
+ event_type_patterns: List[str] = field(default_factory=list)
35
+ session_filter: Optional[Callable[[CrossChannelSession], bool]] = None
36
+ condition: Optional[Callable[[ChannelEvent], bool]] = None
37
+ transform: Optional[Callable[[ChannelEvent], ChannelEvent]] = None
38
+ priority: int = 100 # Lower number = higher priority
39
+ enabled: bool = True
40
+
41
+
42
+ @dataclass
43
+ class RoutingStats:
44
+ """Event routing statistics."""
45
+
46
+ total_events: int = 0
47
+ routed_events: int = 0
48
+ dropped_events: int = 0
49
+ failed_events: int = 0
50
+ routes_matched: Dict[str, int] = field(default_factory=dict)
51
+ channel_stats: Dict[str, Dict[str, int]] = field(default_factory=dict)
52
+
53
+
54
+ class EventRouter:
55
+ """Cross-channel event router for the Nexus framework.
56
+
57
+ This router handles event distribution between different channels,
58
+ enabling unified communication across API, CLI, and MCP interfaces.
59
+ """
60
+
61
+ def __init__(self, session_manager: Optional[SessionManager] = None):
62
+ """Initialize event router.
63
+
64
+ Args:
65
+ session_manager: Optional session manager for session-based routing
66
+ """
67
+ self.session_manager = session_manager
68
+ self._channels: Dict[str, Channel] = {}
69
+ self._routes: List[EventRoute] = []
70
+ self._event_queue: Optional[asyncio.Queue] = None
71
+ self._router_task: Optional[asyncio.Task] = None
72
+ self._running = False
73
+ self._stats = RoutingStats()
74
+
75
+ # Setup default routes
76
+ self._setup_default_routes()
77
+
78
+ logger.info("Event router initialized")
79
+
80
+ def _setup_default_routes(self) -> None:
81
+ """Set up default routing rules."""
82
+
83
+ # Route channel lifecycle events to all other channels
84
+ self.add_route(
85
+ EventRoute(
86
+ rule_type=RoutingRule.BROADCAST,
87
+ event_type_patterns=[
88
+ "channel_started",
89
+ "channel_stopped",
90
+ "channel_error",
91
+ ],
92
+ priority=50,
93
+ )
94
+ )
95
+
96
+ # Route session events to channels in the same session
97
+ self.add_route(
98
+ EventRoute(
99
+ rule_type=RoutingRule.SESSION,
100
+ event_type_patterns=["session_*"],
101
+ priority=60,
102
+ )
103
+ )
104
+
105
+ # Route workflow events based on session
106
+ self.add_route(
107
+ EventRoute(
108
+ rule_type=RoutingRule.SESSION,
109
+ event_type_patterns=["workflow_*", "mcp_*", "command_*"],
110
+ priority=70,
111
+ )
112
+ )
113
+
114
+ async def start(self) -> None:
115
+ """Start the event router."""
116
+ if self._running:
117
+ logger.warning("Event router is already running")
118
+ return
119
+
120
+ self._running = True
121
+ self._event_queue = asyncio.Queue(maxsize=10000)
122
+ self._router_task = asyncio.create_task(self._routing_loop())
123
+
124
+ logger.info("Event router started")
125
+
126
+ async def stop(self) -> None:
127
+ """Stop the event router."""
128
+ if not self._running:
129
+ return
130
+
131
+ self._running = False
132
+
133
+ if self._router_task and not self._router_task.done():
134
+ self._router_task.cancel()
135
+ try:
136
+ await self._router_task
137
+ except asyncio.CancelledError:
138
+ pass
139
+
140
+ # Clear remaining events
141
+ if self._event_queue:
142
+ while not self._event_queue.empty():
143
+ try:
144
+ self._event_queue.get_nowait()
145
+ except asyncio.QueueEmpty:
146
+ break
147
+
148
+ logger.info("Event router stopped")
149
+
150
+ def register_channel(self, channel: Channel) -> None:
151
+ """Register a channel with the router.
152
+
153
+ Args:
154
+ channel: Channel to register
155
+ """
156
+ self._channels[channel.name] = channel
157
+
158
+ # Initialize channel stats
159
+ if channel.name not in self._stats.channel_stats:
160
+ self._stats.channel_stats[channel.name] = {
161
+ "events_sent": 0,
162
+ "events_received": 0,
163
+ "events_failed": 0,
164
+ }
165
+
166
+ logger.info(f"Registered channel '{channel.name}' with event router")
167
+
168
+ def unregister_channel(self, channel_name: str) -> None:
169
+ """Unregister a channel from the router.
170
+
171
+ Args:
172
+ channel_name: Name of channel to unregister
173
+ """
174
+ if channel_name in self._channels:
175
+ del self._channels[channel_name]
176
+ logger.info(f"Unregistered channel '{channel_name}' from event router")
177
+
178
+ def add_route(self, route: EventRoute) -> None:
179
+ """Add a routing rule.
180
+
181
+ Args:
182
+ route: Routing rule to add
183
+ """
184
+ self._routes.append(route)
185
+ # Sort by priority (lower number = higher priority)
186
+ self._routes.sort(key=lambda r: r.priority)
187
+ logger.debug(f"Added routing rule: {route.rule_type.value}")
188
+
189
+ def remove_route(self, route: EventRoute) -> None:
190
+ """Remove a routing rule.
191
+
192
+ Args:
193
+ route: Routing rule to remove
194
+ """
195
+ if route in self._routes:
196
+ self._routes.remove(route)
197
+ logger.debug(f"Removed routing rule: {route.rule_type.value}")
198
+
199
+ async def route_event(self, event: ChannelEvent) -> None:
200
+ """Route an event to appropriate channels.
201
+
202
+ Args:
203
+ event: Event to route
204
+ """
205
+ if not self._running or not self._event_queue:
206
+ logger.warning("Event router not running, dropping event")
207
+ return
208
+
209
+ try:
210
+ await self._event_queue.put(event)
211
+ self._stats.total_events += 1
212
+ except asyncio.QueueFull:
213
+ logger.warning("Event queue full, dropping event")
214
+ self._stats.dropped_events += 1
215
+
216
+ async def _routing_loop(self) -> None:
217
+ """Main event routing loop."""
218
+ while self._running:
219
+ try:
220
+ if not self._event_queue:
221
+ break
222
+
223
+ # Get event with timeout
224
+ try:
225
+ event = await asyncio.wait_for(self._event_queue.get(), timeout=1.0)
226
+ except asyncio.TimeoutError:
227
+ continue
228
+
229
+ await self._process_event(event)
230
+
231
+ except asyncio.CancelledError:
232
+ break
233
+ except Exception as e:
234
+ logger.error(f"Error in routing loop: {e}")
235
+
236
+ async def _process_event(self, event: ChannelEvent) -> None:
237
+ """Process a single event through routing rules.
238
+
239
+ Args:
240
+ event: Event to process
241
+ """
242
+ try:
243
+ # Update stats
244
+ source_channel = event.channel_name
245
+ if source_channel in self._stats.channel_stats:
246
+ self._stats.channel_stats[source_channel]["events_sent"] += 1
247
+
248
+ # Find matching routes
249
+ matching_routes = self._find_matching_routes(event)
250
+
251
+ if not matching_routes:
252
+ logger.debug(f"No routes found for event {event.event_id}")
253
+ self._stats.dropped_events += 1
254
+ return
255
+
256
+ # Apply routes in priority order
257
+ for route in matching_routes:
258
+ try:
259
+ await self._apply_route(event, route)
260
+ self._stats.routed_events += 1
261
+
262
+ # Update route stats
263
+ route_key = f"{route.rule_type.value}_{id(route)}"
264
+ self._stats.routes_matched[route_key] = (
265
+ self._stats.routes_matched.get(route_key, 0) + 1
266
+ )
267
+
268
+ except Exception as e:
269
+ logger.error(f"Error applying route {route.rule_type.value}: {e}")
270
+ self._stats.failed_events += 1
271
+
272
+ except Exception as e:
273
+ logger.error(f"Error processing event {event.event_id}: {e}")
274
+ self._stats.failed_events += 1
275
+
276
+ def _find_matching_routes(self, event: ChannelEvent) -> List[EventRoute]:
277
+ """Find routes that match the given event.
278
+
279
+ Args:
280
+ event: Event to match
281
+
282
+ Returns:
283
+ List of matching routes in priority order
284
+ """
285
+ matching_routes = []
286
+
287
+ for route in self._routes:
288
+ if not route.enabled:
289
+ continue
290
+
291
+ # Check source pattern
292
+ if route.source_patterns and not self._match_patterns(
293
+ event.channel_name, route.source_patterns
294
+ ):
295
+ continue
296
+
297
+ # Check event type pattern
298
+ if route.event_type_patterns and not self._match_patterns(
299
+ event.event_type, route.event_type_patterns
300
+ ):
301
+ continue
302
+
303
+ # Check custom condition
304
+ if route.condition and not route.condition(event):
305
+ continue
306
+
307
+ matching_routes.append(route)
308
+
309
+ return matching_routes
310
+
311
+ def _match_patterns(self, value: str, patterns: List[str]) -> bool:
312
+ """Check if value matches any of the patterns.
313
+
314
+ Args:
315
+ value: Value to match
316
+ patterns: List of patterns (supports wildcards)
317
+
318
+ Returns:
319
+ True if value matches any pattern
320
+ """
321
+ for pattern in patterns:
322
+ if fnmatch.fnmatch(value, pattern):
323
+ return True
324
+ return False
325
+
326
+ async def _apply_route(self, event: ChannelEvent, route: EventRoute) -> None:
327
+ """Apply a routing rule to an event.
328
+
329
+ Args:
330
+ event: Event to route
331
+ route: Route to apply
332
+ """
333
+ # Transform event if needed
334
+ if route.transform:
335
+ event = route.transform(event)
336
+
337
+ # Determine target channels based on route type
338
+ if route.rule_type == RoutingRule.BROADCAST:
339
+ targets = [
340
+ name for name in self._channels.keys() if name != event.channel_name
341
+ ]
342
+ elif route.rule_type == RoutingRule.UNICAST:
343
+ targets = route.target_channels[:1] if route.target_channels else []
344
+ elif route.rule_type == RoutingRule.MULTICAST:
345
+ targets = route.target_channels
346
+ elif route.rule_type == RoutingRule.SESSION:
347
+ targets = await self._find_session_targets(event, route)
348
+ elif route.rule_type == RoutingRule.PATTERN:
349
+ targets = self._find_pattern_targets(event, route)
350
+ else:
351
+ targets = []
352
+
353
+ # Send event to target channels
354
+ for target_name in targets:
355
+ if target_name in self._channels:
356
+ try:
357
+ target_channel = self._channels[target_name]
358
+ await target_channel.handle_event(event)
359
+
360
+ # Update target channel stats
361
+ if target_name in self._stats.channel_stats:
362
+ self._stats.channel_stats[target_name]["events_received"] += 1
363
+
364
+ except Exception as e:
365
+ logger.error(f"Error sending event to channel {target_name}: {e}")
366
+ if target_name in self._stats.channel_stats:
367
+ self._stats.channel_stats[target_name]["events_failed"] += 1
368
+
369
+ async def _find_session_targets(
370
+ self, event: ChannelEvent, route: EventRoute
371
+ ) -> List[str]:
372
+ """Find target channels based on session routing.
373
+
374
+ Args:
375
+ event: Event to route
376
+ route: Route configuration
377
+
378
+ Returns:
379
+ List of target channel names
380
+ """
381
+ if not self.session_manager or not event.session_id:
382
+ return []
383
+
384
+ session = self.session_manager.get_session(event.session_id)
385
+ if not session:
386
+ return []
387
+
388
+ # Apply session filter if provided
389
+ if route.session_filter and not route.session_filter(session):
390
+ return []
391
+
392
+ # Return channels active in the session (excluding source)
393
+ return [
394
+ ch
395
+ for ch in session.active_channels
396
+ if ch != event.channel_name and ch in self._channels
397
+ ]
398
+
399
+ def _find_pattern_targets(
400
+ self, event: ChannelEvent, route: EventRoute
401
+ ) -> List[str]:
402
+ """Find target channels based on pattern matching.
403
+
404
+ Args:
405
+ event: Event to route
406
+ route: Route configuration
407
+
408
+ Returns:
409
+ List of target channel names
410
+ """
411
+ targets = []
412
+
413
+ for channel_name in self._channels.keys():
414
+ if channel_name == event.channel_name:
415
+ continue # Don't send back to source
416
+
417
+ # Check if channel matches target patterns
418
+ if route.target_channels:
419
+ if any(
420
+ self._match_patterns(channel_name, [pattern])
421
+ for pattern in route.target_channels
422
+ ):
423
+ targets.append(channel_name)
424
+
425
+ return targets
426
+
427
+ def get_stats(self) -> Dict[str, Any]:
428
+ """Get event routing statistics.
429
+
430
+ Returns:
431
+ Dictionary with routing statistics
432
+ """
433
+ return {
434
+ "total_events": self._stats.total_events,
435
+ "routed_events": self._stats.routed_events,
436
+ "dropped_events": self._stats.dropped_events,
437
+ "failed_events": self._stats.failed_events,
438
+ "success_rate": (
439
+ self._stats.routed_events / max(1, self._stats.total_events)
440
+ )
441
+ * 100,
442
+ "routes_count": len(self._routes),
443
+ "channels_count": len(self._channels),
444
+ "queue_size": self._event_queue.qsize() if self._event_queue else 0,
445
+ "route_matches": dict(self._stats.routes_matched),
446
+ "channel_stats": dict(self._stats.channel_stats),
447
+ "channels_registered": list(self._channels.keys()),
448
+ }
449
+
450
+ def reset_stats(self) -> None:
451
+ """Reset routing statistics."""
452
+ self._stats = RoutingStats()
453
+ logger.info("Event router statistics reset")
454
+
455
+ async def health_check(self) -> Dict[str, Any]:
456
+ """Perform health check on the event router.
457
+
458
+ Returns:
459
+ Health check results
460
+ """
461
+ try:
462
+ is_healthy = (
463
+ self._running
464
+ and self._event_queue is not None
465
+ and self._router_task is not None
466
+ and not self._router_task.done()
467
+ )
468
+
469
+ queue_health = True
470
+ if self._event_queue:
471
+ queue_size = self._event_queue.qsize()
472
+ queue_health = queue_size < 8000 # Warning if queue getting full
473
+
474
+ return {
475
+ "healthy": is_healthy and queue_health,
476
+ "running": self._running,
477
+ "queue_size": self._event_queue.qsize() if self._event_queue else 0,
478
+ "channels_registered": len(self._channels),
479
+ "routes_configured": len(self._routes),
480
+ "total_events_processed": self._stats.total_events,
481
+ "success_rate": (
482
+ self._stats.routed_events / max(1, self._stats.total_events)
483
+ )
484
+ * 100,
485
+ "checks": {
486
+ "router_running": self._running,
487
+ "queue_available": self._event_queue is not None,
488
+ "task_active": self._router_task is not None
489
+ and not self._router_task.done(),
490
+ "queue_healthy": queue_health,
491
+ "channels_available": len(self._channels) > 0,
492
+ },
493
+ }
494
+
495
+ except Exception as e:
496
+ return {"healthy": False, "error": str(e), "checks": {}}