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
@@ -363,6 +363,13 @@ class LLMAgentNode(Node):
363
363
  default={},
364
364
  description="Configuration for tool execution behavior",
365
365
  ),
366
+ "use_real_mcp": NodeParameter(
367
+ name="use_real_mcp",
368
+ type=bool,
369
+ required=False,
370
+ default=True,
371
+ description="Use real MCP tool execution instead of mock execution",
372
+ ),
366
373
  }
367
374
 
368
375
  def run(self, **kwargs) -> dict[str, Any]:
@@ -629,12 +636,14 @@ class LLMAgentNode(Node):
629
636
  )
630
637
 
631
638
  # Retrieve MCP context if configured
632
- mcp_context_data = self._retrieve_mcp_context(mcp_servers, mcp_context)
639
+ mcp_context_data = self._retrieve_mcp_context(
640
+ mcp_servers, mcp_context, kwargs
641
+ )
633
642
 
634
643
  # Discover MCP tools if enabled
635
644
  discovered_mcp_tools = []
636
645
  if auto_discover_tools and mcp_servers:
637
- discovered_mcp_tools = self._discover_mcp_tools(mcp_servers)
646
+ discovered_mcp_tools = self._discover_mcp_tools(mcp_servers, kwargs)
638
647
  # Merge MCP tools with existing tools
639
648
  tools = self._merge_tools(tools, discovered_mcp_tools)
640
649
 
@@ -976,7 +985,7 @@ class LLMAgentNode(Node):
976
985
  return asyncio.run(coro)
977
986
 
978
987
  def _retrieve_mcp_context(
979
- self, mcp_servers: list[dict], mcp_context: list[str]
988
+ self, mcp_servers: list[dict], mcp_context: list[str], kwargs: dict = None
980
989
  ) -> list[dict[str, Any]]:
981
990
  """
982
991
  Retrieve context from Model Context Protocol (MCP) servers.
@@ -1043,7 +1052,7 @@ class LLMAgentNode(Node):
1043
1052
  context_data = []
1044
1053
 
1045
1054
  # Check if we should use real MCP implementation
1046
- use_real_mcp = hasattr(self, "_mcp_client") or self._should_use_real_mcp()
1055
+ use_real_mcp = hasattr(self, "_mcp_client") or self._should_use_real_mcp(kwargs)
1047
1056
 
1048
1057
  if use_real_mcp:
1049
1058
  # Use internal MCP client for real implementation
@@ -1224,14 +1233,25 @@ class LLMAgentNode(Node):
1224
1233
 
1225
1234
  return context_data
1226
1235
 
1227
- def _should_use_real_mcp(self) -> bool:
1236
+ def _should_use_real_mcp(self, kwargs: dict = None) -> bool:
1228
1237
  """Check if real MCP implementation should be used."""
1229
- # Check environment variable or configuration
1230
1238
  import os
1231
1239
 
1232
- return os.environ.get("KAILASH_USE_REAL_MCP", "false").lower() == "true"
1240
+ # 1. Check explicit parameter first (highest priority)
1241
+ if kwargs and "use_real_mcp" in kwargs:
1242
+ return kwargs["use_real_mcp"]
1233
1243
 
1234
- def _discover_mcp_tools(self, mcp_servers: list[dict]) -> list[dict[str, Any]]:
1244
+ # 2. Check environment variable (fallback)
1245
+ env_value = os.environ.get("KAILASH_USE_REAL_MCP", "").lower()
1246
+ if env_value in ("true", "false"):
1247
+ return env_value == "true"
1248
+
1249
+ # 3. Default to True (real MCP execution)
1250
+ return True
1251
+
1252
+ def _discover_mcp_tools(
1253
+ self, mcp_servers: list[dict], kwargs: dict = None
1254
+ ) -> list[dict[str, Any]]:
1235
1255
  """
1236
1256
  Discover available tools from MCP servers.
1237
1257
 
@@ -1244,7 +1264,7 @@ class LLMAgentNode(Node):
1244
1264
  discovered_tools = []
1245
1265
 
1246
1266
  # Check if we should use real MCP implementation
1247
- use_real_mcp = hasattr(self, "_mcp_client") or self._should_use_real_mcp()
1267
+ use_real_mcp = hasattr(self, "_mcp_client") or self._should_use_real_mcp(kwargs)
1248
1268
 
1249
1269
  if use_real_mcp:
1250
1270
  try:
@@ -23,7 +23,7 @@ import warnings
23
23
  from .auth import APIKeyNode, BasicAuthNode, OAuth2Node
24
24
  from .graphql import AsyncGraphQLClientNode, GraphQLClientNode
25
25
  from .http import AsyncHTTPRequestNode, HTTPRequestNode
26
- from .monitoring import HealthCheckNode
26
+ from .monitoring import APIHealthCheckNode
27
27
  from .rate_limiting import (
28
28
  AsyncRateLimitedAPINode,
29
29
  RateLimitConfig,
@@ -71,7 +71,7 @@ __all__ = [
71
71
  "AsyncRateLimitedAPINode",
72
72
  "create_rate_limiter",
73
73
  # Monitoring and Security
74
- "HealthCheckNode",
74
+ "APIHealthCheckNode",
75
75
  "SecurityScannerNode",
76
76
  # Backwards compatibility
77
77
  "HTTPClientNode", # Deprecated alias
@@ -13,7 +13,7 @@ from kailash.nodes.base import Node, NodeParameter, register_node
13
13
 
14
14
 
15
15
  @register_node()
16
- class HealthCheckNode(Node):
16
+ class APIHealthCheckNode(Node):
17
17
  """
18
18
  Performs health checks on various system components and services.
19
19
 
@@ -46,10 +46,13 @@ class AsyncNode(Node):
46
46
  """
47
47
 
48
48
  def execute(self, **runtime_inputs) -> dict[str, Any]:
49
- """Execute the node synchronously by running async code in a new event loop.
49
+ """Execute the node synchronously by running async code with proper event loop handling.
50
50
 
51
- This override allows AsyncNode to work with synchronous runtimes like LocalRuntime
52
- by wrapping the async execution in a synchronous interface.
51
+ This enhanced implementation handles all event loop scenarios:
52
+ 1. No event loop: Create new one with asyncio.run()
53
+ 2. Event loop running: Use ThreadPoolExecutor with isolated loop
54
+ 3. Threaded contexts: Proper thread-safe execution
55
+ 4. Windows compatibility: ProactorEventLoopPolicy support
53
56
 
54
57
  Args:
55
58
  **runtime_inputs: Runtime inputs for node execution
@@ -62,7 +65,9 @@ class AsyncNode(Node):
62
65
  NodeExecutionError: If execution fails
63
66
  """
64
67
  import asyncio
68
+ import concurrent.futures
65
69
  import sys
70
+ import threading
66
71
 
67
72
  # For sync execution, we always create a new event loop
68
73
  # This avoids complexity with nested loops and ensures clean execution
@@ -70,22 +75,57 @@ class AsyncNode(Node):
70
75
  # Windows requires special handling
71
76
  asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
72
77
 
78
+ # Check if we're in a thread without an event loop
79
+ current_thread = threading.current_thread()
80
+ is_main_thread = isinstance(current_thread, threading._MainThread)
81
+
73
82
  # Run the async method - handle existing event loop
74
83
  try:
75
84
  # Try to get current event loop
76
85
  loop = asyncio.get_running_loop()
86
+ # Event loop is running - need to run in separate thread
87
+ return self._execute_in_thread(**runtime_inputs)
77
88
  except RuntimeError:
78
- # No event loop running, safe to use asyncio.run()
79
- return asyncio.run(self.execute_async(**runtime_inputs))
80
- else:
81
- # Event loop is running, create a task
82
- import concurrent.futures
83
-
84
- with concurrent.futures.ThreadPoolExecutor() as executor:
85
- future = executor.submit(
86
- asyncio.run, self.execute_async(**runtime_inputs)
87
- )
88
- return future.result()
89
+ # No event loop running
90
+ if is_main_thread:
91
+ # Main thread without loop - safe to use asyncio.run()
92
+ return asyncio.run(self.execute_async(**runtime_inputs))
93
+ else:
94
+ # Non-main thread without loop - create new loop
95
+ return self._execute_in_new_loop(**runtime_inputs)
96
+
97
+ def _execute_in_thread(self, **runtime_inputs) -> dict[str, Any]:
98
+ """Execute async code in a separate thread with its own event loop."""
99
+ import asyncio
100
+ import concurrent.futures
101
+
102
+ def run_in_new_loop():
103
+ """Run async code in a completely new event loop."""
104
+ # Create fresh event loop for this thread
105
+ new_loop = asyncio.new_event_loop()
106
+ asyncio.set_event_loop(new_loop)
107
+ try:
108
+ return new_loop.run_until_complete(self.execute_async(**runtime_inputs))
109
+ finally:
110
+ new_loop.close()
111
+ asyncio.set_event_loop(None)
112
+
113
+ with concurrent.futures.ThreadPoolExecutor() as executor:
114
+ future = executor.submit(run_in_new_loop)
115
+ return future.result()
116
+
117
+ def _execute_in_new_loop(self, **runtime_inputs) -> dict[str, Any]:
118
+ """Execute async code by creating a new event loop in current thread."""
119
+ import asyncio
120
+
121
+ # Create and set new event loop for this thread
122
+ loop = asyncio.new_event_loop()
123
+ asyncio.set_event_loop(loop)
124
+ try:
125
+ return loop.run_until_complete(self.execute_async(**runtime_inputs))
126
+ finally:
127
+ loop.close()
128
+ asyncio.set_event_loop(None)
89
129
 
90
130
  def run(self, **kwargs) -> dict[str, Any]:
91
131
  """Synchronous run is not supported for AsyncNode.
@@ -783,7 +783,7 @@ class AsyncPythonCodeNode(AsyncNode):
783
783
  "max_concurrent_tasks",
784
784
  "max_memory_mb",
785
785
  "imports",
786
- "config",
786
+ # Note: "config" removed - it's a valid runtime parameter name
787
787
  }
788
788
  runtime_inputs = {k: v for k, v in kwargs.items() if k not in config_params}
789
789