kailash 0.6.6__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.
- kailash/__init__.py +35 -5
- kailash/adapters/__init__.py +5 -0
- kailash/adapters/mcp_platform_adapter.py +273 -0
- 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 +1 -1
- kailash/middleware/mcp/enhanced_server.py +22 -16
- kailash/nexus/__init__.py +21 -0
- kailash/nexus/factory.py +413 -0
- kailash/nexus/gateway.py +545 -0
- kailash/nodes/__init__.py +2 -0
- 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_async.py +54 -14
- kailash/nodes/code/async_python.py +1 -1
- 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/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/async_local.py +84 -21
- kailash/runtime/local.py +21 -2
- kailash/runtime/parameter_injector.py +187 -31
- kailash/security.py +16 -1
- kailash/servers/__init__.py +32 -0
- kailash/servers/durable_workflow_server.py +430 -0
- kailash/servers/enterprise_workflow_server.py +466 -0
- kailash/servers/gateway.py +183 -0
- kailash/servers/workflow_server.py +290 -0
- kailash/utils/data_validation.py +192 -0
- kailash/workflow/builder.py +291 -12
- kailash/workflow/validation.py +144 -8
- {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/METADATA +1 -1
- {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/RECORD +63 -25
- {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/WHEEL +0 -0
- {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/top_level.txt +0 -0
kailash/nodes/ai/llm_agent.py
CHANGED
@@ -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(
|
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
|
-
|
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
|
-
|
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:
|
kailash/nodes/api/__init__.py
CHANGED
@@ -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
|
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
|
-
"
|
74
|
+
"APIHealthCheckNode",
|
75
75
|
"SecurityScannerNode",
|
76
76
|
# Backwards compatibility
|
77
77
|
"HTTPClientNode", # Deprecated alias
|
kailash/nodes/api/monitoring.py
CHANGED
kailash/nodes/base_async.py
CHANGED
@@ -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
|
49
|
+
"""Execute the node synchronously by running async code with proper event loop handling.
|
50
50
|
|
51
|
-
This
|
52
|
-
|
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
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
|