kailash 0.1.4__py3-none-any.whl → 0.2.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 +1 -1
- kailash/access_control.py +740 -0
- kailash/api/__main__.py +6 -0
- kailash/api/auth.py +668 -0
- kailash/api/custom_nodes.py +285 -0
- kailash/api/custom_nodes_secure.py +377 -0
- kailash/api/database.py +620 -0
- kailash/api/studio.py +915 -0
- kailash/api/studio_secure.py +893 -0
- kailash/mcp/__init__.py +53 -0
- kailash/mcp/__main__.py +13 -0
- kailash/mcp/ai_registry_server.py +712 -0
- kailash/mcp/client.py +447 -0
- kailash/mcp/client_new.py +334 -0
- kailash/mcp/server.py +293 -0
- kailash/mcp/server_new.py +336 -0
- kailash/mcp/servers/__init__.py +12 -0
- kailash/mcp/servers/ai_registry.py +289 -0
- kailash/nodes/__init__.py +4 -2
- kailash/nodes/ai/__init__.py +38 -0
- kailash/nodes/ai/a2a.py +1790 -0
- kailash/nodes/ai/agents.py +116 -2
- kailash/nodes/ai/ai_providers.py +206 -8
- kailash/nodes/ai/intelligent_agent_orchestrator.py +2108 -0
- kailash/nodes/ai/iterative_llm_agent.py +1280 -0
- kailash/nodes/ai/llm_agent.py +324 -1
- kailash/nodes/ai/self_organizing.py +1623 -0
- kailash/nodes/api/http.py +106 -25
- kailash/nodes/api/rest.py +116 -21
- kailash/nodes/base.py +15 -2
- kailash/nodes/base_async.py +45 -0
- kailash/nodes/base_cycle_aware.py +374 -0
- kailash/nodes/base_with_acl.py +338 -0
- kailash/nodes/code/python.py +135 -27
- kailash/nodes/data/readers.py +116 -53
- kailash/nodes/data/writers.py +16 -6
- kailash/nodes/logic/__init__.py +8 -0
- kailash/nodes/logic/async_operations.py +48 -9
- kailash/nodes/logic/convergence.py +642 -0
- kailash/nodes/logic/loop.py +153 -0
- kailash/nodes/logic/operations.py +212 -27
- kailash/nodes/logic/workflow.py +26 -18
- kailash/nodes/mixins/__init__.py +11 -0
- kailash/nodes/mixins/mcp.py +228 -0
- kailash/nodes/mixins.py +387 -0
- kailash/nodes/transform/__init__.py +8 -1
- kailash/nodes/transform/processors.py +119 -4
- kailash/runtime/__init__.py +2 -1
- kailash/runtime/access_controlled.py +458 -0
- kailash/runtime/local.py +106 -33
- kailash/runtime/parallel_cyclic.py +529 -0
- kailash/sdk_exceptions.py +90 -5
- kailash/security.py +845 -0
- kailash/tracking/manager.py +38 -15
- kailash/tracking/models.py +1 -1
- kailash/tracking/storage/filesystem.py +30 -2
- kailash/utils/__init__.py +8 -0
- kailash/workflow/__init__.py +18 -0
- kailash/workflow/convergence.py +270 -0
- kailash/workflow/cycle_analyzer.py +768 -0
- kailash/workflow/cycle_builder.py +573 -0
- kailash/workflow/cycle_config.py +709 -0
- kailash/workflow/cycle_debugger.py +760 -0
- kailash/workflow/cycle_exceptions.py +601 -0
- kailash/workflow/cycle_profiler.py +671 -0
- kailash/workflow/cycle_state.py +338 -0
- kailash/workflow/cyclic_runner.py +985 -0
- kailash/workflow/graph.py +500 -39
- kailash/workflow/migration.py +768 -0
- kailash/workflow/safety.py +365 -0
- kailash/workflow/templates.py +744 -0
- kailash/workflow/validation.py +693 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/METADATA +446 -13
- kailash-0.2.0.dist-info/RECORD +125 -0
- kailash/nodes/mcp/__init__.py +0 -11
- kailash/nodes/mcp/client.py +0 -554
- kailash/nodes/mcp/resource.py +0 -682
- kailash/nodes/mcp/server.py +0 -577
- kailash-0.1.4.dist-info/RECORD +0 -85
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
kailash/nodes/ai/llm_agent.py
CHANGED
@@ -182,6 +182,13 @@ class LLMAgentNode(Node):
|
|
182
182
|
default=[],
|
183
183
|
description="MCP resource URIs to include as context",
|
184
184
|
),
|
185
|
+
"auto_discover_tools": NodeParameter(
|
186
|
+
name="auto_discover_tools",
|
187
|
+
type=bool,
|
188
|
+
required=False,
|
189
|
+
default=False,
|
190
|
+
description="Automatically discover and use MCP tools",
|
191
|
+
),
|
185
192
|
"rag_config": NodeParameter(
|
186
193
|
name="rag_config",
|
187
194
|
type=dict,
|
@@ -451,6 +458,7 @@ class LLMAgentNode(Node):
|
|
451
458
|
memory_config = kwargs.get("memory_config", {})
|
452
459
|
mcp_servers = kwargs.get("mcp_servers", [])
|
453
460
|
mcp_context = kwargs.get("mcp_context", [])
|
461
|
+
auto_discover_tools = kwargs.get("auto_discover_tools", False)
|
454
462
|
rag_config = kwargs.get("rag_config", {})
|
455
463
|
generation_config = kwargs.get("generation_config", {})
|
456
464
|
streaming = kwargs.get("streaming", False)
|
@@ -469,6 +477,12 @@ class LLMAgentNode(Node):
|
|
469
477
|
# Retrieve MCP context if configured
|
470
478
|
mcp_context_data = self._retrieve_mcp_context(mcp_servers, mcp_context)
|
471
479
|
|
480
|
+
# Discover MCP tools if enabled
|
481
|
+
if auto_discover_tools and mcp_servers:
|
482
|
+
mcp_tools = self._discover_mcp_tools(mcp_servers)
|
483
|
+
# Merge MCP tools with existing tools
|
484
|
+
tools = self._merge_tools(tools, mcp_tools)
|
485
|
+
|
472
486
|
# Perform RAG retrieval if configured
|
473
487
|
rag_context = self._perform_rag_retrieval(
|
474
488
|
messages, rag_config, mcp_context_data
|
@@ -727,7 +741,125 @@ class LLMAgentNode(Node):
|
|
727
741
|
|
728
742
|
context_data = []
|
729
743
|
|
730
|
-
#
|
744
|
+
# Check if we should use real MCP implementation
|
745
|
+
use_real_mcp = hasattr(self, "_mcp_client") or self._should_use_real_mcp()
|
746
|
+
|
747
|
+
if use_real_mcp:
|
748
|
+
# Use internal MCP client for real implementation
|
749
|
+
try:
|
750
|
+
import asyncio
|
751
|
+
from datetime import datetime
|
752
|
+
|
753
|
+
from kailash.mcp import MCPClient
|
754
|
+
|
755
|
+
# Initialize MCP client if not already done
|
756
|
+
if not hasattr(self, "_mcp_client"):
|
757
|
+
self._mcp_client = MCPClient()
|
758
|
+
|
759
|
+
# Process each server
|
760
|
+
for server_config in mcp_servers:
|
761
|
+
try:
|
762
|
+
# List resources from server
|
763
|
+
resources = asyncio.run(
|
764
|
+
self._mcp_client.list_resources(server_config)
|
765
|
+
)
|
766
|
+
|
767
|
+
# Read specific resources if requested
|
768
|
+
for uri in mcp_context:
|
769
|
+
try:
|
770
|
+
resource_data = asyncio.run(
|
771
|
+
self._mcp_client.read_resource(server_config, uri)
|
772
|
+
)
|
773
|
+
|
774
|
+
if resource_data:
|
775
|
+
# Extract content from resource data
|
776
|
+
content = resource_data
|
777
|
+
if isinstance(resource_data, dict):
|
778
|
+
content = resource_data.get(
|
779
|
+
"content", resource_data
|
780
|
+
)
|
781
|
+
|
782
|
+
# Handle different content formats
|
783
|
+
if isinstance(content, list):
|
784
|
+
# MCP returns content as array of items
|
785
|
+
text_content = ""
|
786
|
+
for item in content:
|
787
|
+
if (
|
788
|
+
isinstance(item, dict)
|
789
|
+
and item.get("type") == "text"
|
790
|
+
):
|
791
|
+
text_content += item.get("text", "")
|
792
|
+
elif isinstance(item, str):
|
793
|
+
text_content += item
|
794
|
+
content = text_content
|
795
|
+
|
796
|
+
context_data.append(
|
797
|
+
{
|
798
|
+
"uri": uri,
|
799
|
+
"content": str(content),
|
800
|
+
"source": server_config.get(
|
801
|
+
"name", "mcp_server"
|
802
|
+
),
|
803
|
+
"retrieved_at": datetime.now().isoformat(),
|
804
|
+
"relevance_score": 0.95, # High score for explicitly requested
|
805
|
+
"metadata": (
|
806
|
+
resource_data
|
807
|
+
if isinstance(resource_data, dict)
|
808
|
+
else {}
|
809
|
+
),
|
810
|
+
}
|
811
|
+
)
|
812
|
+
except Exception as e:
|
813
|
+
self.logger.debug(f"Failed to read resource {uri}: {e}")
|
814
|
+
|
815
|
+
# Auto-discover and include relevant resources
|
816
|
+
if resources and isinstance(resources, list):
|
817
|
+
for resource in resources[
|
818
|
+
:3
|
819
|
+
]: # Limit auto-discovered resources
|
820
|
+
resource_dict = (
|
821
|
+
resource
|
822
|
+
if isinstance(resource, dict)
|
823
|
+
else {"uri": str(resource)}
|
824
|
+
)
|
825
|
+
context_data.append(
|
826
|
+
{
|
827
|
+
"uri": resource_dict.get("uri", ""),
|
828
|
+
"content": f"Auto-discovered: {resource_dict.get('name', '')} - {resource_dict.get('description', '')}",
|
829
|
+
"source": server_config.get(
|
830
|
+
"name", "mcp_server"
|
831
|
+
),
|
832
|
+
"retrieved_at": datetime.now().isoformat(),
|
833
|
+
"relevance_score": 0.75,
|
834
|
+
"metadata": resource_dict,
|
835
|
+
}
|
836
|
+
)
|
837
|
+
|
838
|
+
except Exception as e:
|
839
|
+
self.logger.debug(f"MCP server connection failed: {e}")
|
840
|
+
# Fall back to mock for this server
|
841
|
+
context_data.append(
|
842
|
+
{
|
843
|
+
"uri": f"mcp://{server_config.get('name', 'unknown')}/fallback",
|
844
|
+
"content": "Connection failed, using fallback content",
|
845
|
+
"source": server_config.get("name", "unknown"),
|
846
|
+
"retrieved_at": datetime.now().isoformat(),
|
847
|
+
"relevance_score": 0.5,
|
848
|
+
"metadata": {"error": str(e)},
|
849
|
+
}
|
850
|
+
)
|
851
|
+
|
852
|
+
# If we got real data, return it
|
853
|
+
if context_data:
|
854
|
+
return context_data
|
855
|
+
|
856
|
+
except ImportError:
|
857
|
+
# MCPClient not available, fall back to mock
|
858
|
+
pass
|
859
|
+
except Exception as e:
|
860
|
+
self.logger.debug(f"MCP retrieval error: {e}")
|
861
|
+
|
862
|
+
# Fallback to mock implementation
|
731
863
|
for uri in mcp_context:
|
732
864
|
context_data.append(
|
733
865
|
{
|
@@ -754,6 +886,146 @@ class LLMAgentNode(Node):
|
|
754
886
|
|
755
887
|
return context_data
|
756
888
|
|
889
|
+
def _should_use_real_mcp(self) -> bool:
|
890
|
+
"""Check if real MCP implementation should be used."""
|
891
|
+
# Check environment variable or configuration
|
892
|
+
import os
|
893
|
+
|
894
|
+
return os.environ.get("KAILASH_USE_REAL_MCP", "false").lower() == "true"
|
895
|
+
|
896
|
+
def _discover_mcp_tools(self, mcp_servers: List[dict]) -> List[Dict[str, Any]]:
|
897
|
+
"""
|
898
|
+
Discover available tools from MCP servers.
|
899
|
+
|
900
|
+
Args:
|
901
|
+
mcp_servers: List of MCP server configurations
|
902
|
+
|
903
|
+
Returns:
|
904
|
+
List of tool definitions in OpenAI function calling format
|
905
|
+
"""
|
906
|
+
discovered_tools = []
|
907
|
+
|
908
|
+
# Check if we should use real MCP implementation
|
909
|
+
use_real_mcp = hasattr(self, "_mcp_client") or self._should_use_real_mcp()
|
910
|
+
|
911
|
+
if use_real_mcp:
|
912
|
+
try:
|
913
|
+
import asyncio
|
914
|
+
|
915
|
+
from kailash.mcp import MCPClient
|
916
|
+
|
917
|
+
# Initialize MCP client if not already done
|
918
|
+
if not hasattr(self, "_mcp_client"):
|
919
|
+
self._mcp_client = MCPClient()
|
920
|
+
|
921
|
+
# Discover tools from each server
|
922
|
+
for server_config in mcp_servers:
|
923
|
+
try:
|
924
|
+
# Discover tools asynchronously
|
925
|
+
tools = asyncio.run(
|
926
|
+
self._mcp_client.discover_tools(server_config)
|
927
|
+
)
|
928
|
+
|
929
|
+
# Convert MCP tools to OpenAI function calling format
|
930
|
+
if isinstance(tools, list):
|
931
|
+
for tool in tools:
|
932
|
+
tool_dict = (
|
933
|
+
tool
|
934
|
+
if isinstance(tool, dict)
|
935
|
+
else {"name": str(tool)}
|
936
|
+
)
|
937
|
+
# Extract tool info
|
938
|
+
function_def = {
|
939
|
+
"name": tool_dict.get("name", "unknown"),
|
940
|
+
"description": tool_dict.get("description", ""),
|
941
|
+
"parameters": tool_dict.get(
|
942
|
+
"inputSchema", tool_dict.get("parameters", {})
|
943
|
+
),
|
944
|
+
}
|
945
|
+
# Add MCP metadata
|
946
|
+
function_def["mcp_server"] = server_config.get(
|
947
|
+
"name", "mcp_server"
|
948
|
+
)
|
949
|
+
function_def["mcp_server_config"] = server_config
|
950
|
+
|
951
|
+
discovered_tools.append(
|
952
|
+
{"type": "function", "function": function_def}
|
953
|
+
)
|
954
|
+
|
955
|
+
except Exception as e:
|
956
|
+
self.logger.debug(
|
957
|
+
f"Failed to discover tools from {server_config.get('name', 'unknown')}: {e}"
|
958
|
+
)
|
959
|
+
|
960
|
+
except ImportError:
|
961
|
+
# MCPClient not available, use mock tools
|
962
|
+
pass
|
963
|
+
except Exception as e:
|
964
|
+
self.logger.debug(f"MCP tool discovery error: {e}")
|
965
|
+
|
966
|
+
# If no real tools discovered, provide minimal generic tools
|
967
|
+
if not discovered_tools:
|
968
|
+
# Provide minimal generic tools for each server
|
969
|
+
for server_config in mcp_servers:
|
970
|
+
server_name = server_config.get("name", "mcp_server")
|
971
|
+
discovered_tools.extend(
|
972
|
+
[
|
973
|
+
{
|
974
|
+
"type": "function",
|
975
|
+
"function": {
|
976
|
+
"name": f"mcp_{server_name}_search",
|
977
|
+
"description": f"Search for information in {server_name}",
|
978
|
+
"parameters": {
|
979
|
+
"type": "object",
|
980
|
+
"properties": {
|
981
|
+
"query": {
|
982
|
+
"type": "string",
|
983
|
+
"description": "Search query",
|
984
|
+
}
|
985
|
+
},
|
986
|
+
"required": ["query"],
|
987
|
+
},
|
988
|
+
"mcp_server": server_name,
|
989
|
+
"mcp_server_config": server_config,
|
990
|
+
},
|
991
|
+
}
|
992
|
+
]
|
993
|
+
)
|
994
|
+
|
995
|
+
return discovered_tools
|
996
|
+
|
997
|
+
def _merge_tools(
|
998
|
+
self, existing_tools: List[dict], mcp_tools: List[dict]
|
999
|
+
) -> List[dict]:
|
1000
|
+
"""
|
1001
|
+
Merge MCP discovered tools with existing tools, avoiding duplicates.
|
1002
|
+
|
1003
|
+
Args:
|
1004
|
+
existing_tools: Tools already defined
|
1005
|
+
mcp_tools: Tools discovered from MCP servers
|
1006
|
+
|
1007
|
+
Returns:
|
1008
|
+
Merged list of tools
|
1009
|
+
"""
|
1010
|
+
# Create a set of existing tool names for deduplication
|
1011
|
+
existing_names = set()
|
1012
|
+
for tool in existing_tools:
|
1013
|
+
if isinstance(tool, dict) and "function" in tool:
|
1014
|
+
existing_names.add(tool["function"].get("name", ""))
|
1015
|
+
elif isinstance(tool, dict) and "name" in tool:
|
1016
|
+
existing_names.add(tool["name"])
|
1017
|
+
|
1018
|
+
# Add MCP tools that don't conflict
|
1019
|
+
merged_tools = existing_tools.copy()
|
1020
|
+
for mcp_tool in mcp_tools:
|
1021
|
+
if isinstance(mcp_tool, dict) and "function" in mcp_tool:
|
1022
|
+
tool_name = mcp_tool["function"].get("name", "")
|
1023
|
+
if tool_name and tool_name not in existing_names:
|
1024
|
+
merged_tools.append(mcp_tool)
|
1025
|
+
existing_names.add(tool_name)
|
1026
|
+
|
1027
|
+
return merged_tools
|
1028
|
+
|
757
1029
|
def _perform_rag_retrieval(
|
758
1030
|
self, messages: List[dict], rag_config: dict, mcp_context: List[dict]
|
759
1031
|
) -> Dict[str, Any]:
|
@@ -1159,3 +1431,54 @@ class LLMAgentNode(Node):
|
|
1159
1431
|
"provider": provider,
|
1160
1432
|
"efficiency_score": completion_tokens / max(total_tokens, 1),
|
1161
1433
|
}
|
1434
|
+
|
1435
|
+
async def _execute_mcp_tool_call(
|
1436
|
+
self, tool_call: dict, mcp_tools: List[dict]
|
1437
|
+
) -> Dict[str, Any]:
|
1438
|
+
"""Execute an MCP tool call.
|
1439
|
+
|
1440
|
+
Args:
|
1441
|
+
tool_call: Tool call from LLM response
|
1442
|
+
mcp_tools: List of discovered MCP tools
|
1443
|
+
|
1444
|
+
Returns:
|
1445
|
+
Tool execution result
|
1446
|
+
"""
|
1447
|
+
tool_name = tool_call.get("function", {}).get("name", "")
|
1448
|
+
tool_args = json.loads(tool_call.get("function", {}).get("arguments", "{}"))
|
1449
|
+
|
1450
|
+
# Find the MCP tool definition
|
1451
|
+
mcp_tool = None
|
1452
|
+
for tool in mcp_tools:
|
1453
|
+
if tool.get("function", {}).get("name") == tool_name:
|
1454
|
+
mcp_tool = tool
|
1455
|
+
break
|
1456
|
+
|
1457
|
+
if not mcp_tool:
|
1458
|
+
return {"error": f"MCP tool '{tool_name}' not found", "success": False}
|
1459
|
+
|
1460
|
+
# Get server config from tool
|
1461
|
+
server_config = mcp_tool.get("function", {}).get("mcp_server_config", {})
|
1462
|
+
|
1463
|
+
try:
|
1464
|
+
from kailash.mcp import MCPClient
|
1465
|
+
|
1466
|
+
# Initialize MCP client if not already done
|
1467
|
+
if not hasattr(self, "_mcp_client"):
|
1468
|
+
self._mcp_client = MCPClient()
|
1469
|
+
|
1470
|
+
# Call the tool
|
1471
|
+
result = await self._mcp_client.call_tool(
|
1472
|
+
server_config, tool_name, tool_args
|
1473
|
+
)
|
1474
|
+
|
1475
|
+
return {
|
1476
|
+
"result": result,
|
1477
|
+
"success": True,
|
1478
|
+
"tool_name": tool_name,
|
1479
|
+
"server": server_config.get("name", "unknown"),
|
1480
|
+
}
|
1481
|
+
|
1482
|
+
except Exception as e:
|
1483
|
+
self.logger.error(f"MCP tool execution failed: {e}")
|
1484
|
+
return {"error": str(e), "success": False, "tool_name": tool_name}
|