kailash 0.1.2__py3-none-any.whl → 0.1.4__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 (48) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/api/__init__.py +17 -0
  3. kailash/api/gateway.py +394 -0
  4. kailash/api/mcp_integration.py +478 -0
  5. kailash/api/workflow_api.py +399 -0
  6. kailash/nodes/ai/__init__.py +4 -4
  7. kailash/nodes/ai/agents.py +4 -4
  8. kailash/nodes/ai/ai_providers.py +18 -22
  9. kailash/nodes/ai/embedding_generator.py +34 -38
  10. kailash/nodes/ai/llm_agent.py +351 -356
  11. kailash/nodes/api/http.py +0 -4
  12. kailash/nodes/api/rest.py +1 -1
  13. kailash/nodes/base.py +60 -64
  14. kailash/nodes/code/python.py +61 -42
  15. kailash/nodes/data/__init__.py +10 -10
  16. kailash/nodes/data/readers.py +27 -29
  17. kailash/nodes/data/retrieval.py +1 -1
  18. kailash/nodes/data/sharepoint_graph.py +23 -25
  19. kailash/nodes/data/sql.py +27 -29
  20. kailash/nodes/data/vector_db.py +2 -2
  21. kailash/nodes/data/writers.py +41 -44
  22. kailash/nodes/logic/__init__.py +10 -3
  23. kailash/nodes/logic/async_operations.py +14 -14
  24. kailash/nodes/logic/operations.py +18 -22
  25. kailash/nodes/logic/workflow.py +439 -0
  26. kailash/nodes/mcp/client.py +29 -33
  27. kailash/nodes/mcp/resource.py +1 -1
  28. kailash/nodes/mcp/server.py +10 -4
  29. kailash/nodes/transform/formatters.py +1 -1
  30. kailash/nodes/transform/processors.py +5 -3
  31. kailash/runtime/docker.py +2 -0
  32. kailash/tracking/metrics_collector.py +6 -7
  33. kailash/tracking/models.py +0 -20
  34. kailash/tracking/storage/database.py +4 -4
  35. kailash/tracking/storage/filesystem.py +0 -1
  36. kailash/utils/export.py +2 -2
  37. kailash/utils/templates.py +16 -16
  38. kailash/visualization/performance.py +7 -7
  39. kailash/visualization/reports.py +1 -1
  40. kailash/workflow/graph.py +4 -4
  41. kailash/workflow/mock_registry.py +1 -1
  42. {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/METADATA +198 -27
  43. kailash-0.1.4.dist-info/RECORD +85 -0
  44. kailash-0.1.2.dist-info/RECORD +0 -80
  45. {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/WHEEL +0 -0
  46. {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/entry_points.txt +0 -0
  47. {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/licenses/LICENSE +0 -0
  48. {kailash-0.1.2.dist-info → kailash-0.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,478 @@
1
+ """MCP (Model Context Protocol) integration for Kailash workflows.
2
+
3
+ This module provides integration between MCP servers and Kailash workflows,
4
+ allowing workflows to access MCP tools and resources.
5
+
6
+ Design Philosophy:
7
+ MCP servers provide tools and context that can be used by workflows.
8
+ This integration allows workflows to discover and use MCP tools dynamically,
9
+ bridging the gap between workflow automation and AI-powered tools.
10
+
11
+ Example:
12
+ Basic MCP integration:
13
+
14
+ >>> from kailash.api.mcp_integration import MCPIntegration
15
+ >>> # from kailash.workflow import Workflow # doctest: +SKIP
16
+ >>> # Create MCP integration
17
+ >>> mcp = MCPIntegration("tools_server")
18
+ >>> # Add tools
19
+ >>> # mcp.add_tool("search", search_function) # doctest: +SKIP
20
+ >>> # mcp.add_tool("calculate", calculator_function) # doctest: +SKIP
21
+ >>> # Use in workflow
22
+ >>> # workflow = Workflow("mcp_workflow") # doctest: +SKIP
23
+ >>> # workflow.register_mcp_server(mcp) # doctest: +SKIP
24
+ >>> # Nodes can now access MCP tools
25
+ >>> # node = MCPToolNode(tool_name="search") # doctest: +SKIP
26
+ >>> # workflow.add_node("search_data", node) # doctest: +SKIP
27
+ """
28
+
29
+ import asyncio
30
+ import logging
31
+ from typing import Any, Callable, Dict, List, Optional, Union
32
+
33
+ from pydantic import BaseModel, Field
34
+
35
+ from ..nodes.base_async import AsyncNode
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class MCPTool(BaseModel):
41
+ """Definition of an MCP tool."""
42
+
43
+ name: str
44
+ description: str
45
+ parameters: Dict[str, Any] = Field(default_factory=dict)
46
+ function: Optional[Callable] = None
47
+ async_function: Optional[Callable] = None
48
+ metadata: Dict[str, Any] = Field(default_factory=dict)
49
+
50
+
51
+ class MCPResource(BaseModel):
52
+ """Definition of an MCP resource."""
53
+
54
+ name: str
55
+ uri: str
56
+ description: str
57
+ mime_type: str = "text/plain"
58
+ metadata: Dict[str, Any] = Field(default_factory=dict)
59
+
60
+
61
+ class MCPIntegration:
62
+ """MCP server integration for Kailash workflows.
63
+
64
+ Provides:
65
+ - Tool registration and discovery
66
+ - Resource management
67
+ - Async/sync tool execution
68
+ - Context sharing between workflows and MCP
69
+
70
+ Attributes:
71
+ name: MCP server name
72
+ tools: Registry of available tools
73
+ resources: Registry of available resources
74
+ """
75
+
76
+ def __init__(
77
+ self, name: str, description: str = "", capabilities: List[str] = None
78
+ ):
79
+ """Initialize MCP integration.
80
+
81
+ Args:
82
+ name: MCP server name
83
+ description: Server description
84
+ capabilities: List of server capabilities
85
+ """
86
+ self.name = name
87
+ self.description = description
88
+ self.capabilities = capabilities or ["tools", "resources"]
89
+ self.tools: Dict[str, MCPTool] = {}
90
+ self.resources: Dict[str, MCPResource] = {}
91
+ self._context: Dict[str, Any] = {}
92
+
93
+ def add_tool(
94
+ self,
95
+ name: str,
96
+ function: Union[Callable, Callable[..., asyncio.Future]],
97
+ description: str = "",
98
+ parameters: Dict[str, Any] = None,
99
+ ):
100
+ """Add a tool to the MCP server.
101
+
102
+ Args:
103
+ name: Tool name
104
+ function: Tool implementation (sync or async)
105
+ description: Tool description
106
+ parameters: Tool parameter schema
107
+ """
108
+ tool = MCPTool(
109
+ name=name,
110
+ description=description,
111
+ parameters=parameters or {},
112
+ function=function if not asyncio.iscoroutinefunction(function) else None,
113
+ async_function=function if asyncio.iscoroutinefunction(function) else None,
114
+ )
115
+
116
+ self.tools[name] = tool
117
+ logger.info(f"Added tool '{name}' to MCP server '{self.name}'")
118
+
119
+ def add_resource(
120
+ self, name: str, uri: str, description: str = "", mime_type: str = "text/plain"
121
+ ):
122
+ """Add a resource to the MCP server.
123
+
124
+ Args:
125
+ name: Resource name
126
+ uri: Resource URI
127
+ description: Resource description
128
+ mime_type: MIME type of the resource
129
+ """
130
+ resource = MCPResource(
131
+ name=name, uri=uri, description=description, mime_type=mime_type
132
+ )
133
+
134
+ self.resources[name] = resource
135
+ logger.info(f"Added resource '{name}' to MCP server '{self.name}'")
136
+
137
+ async def execute_tool(self, tool_name: str, parameters: Dict[str, Any]) -> Any:
138
+ """Execute a tool asynchronously.
139
+
140
+ Args:
141
+ tool_name: Name of the tool to execute
142
+ parameters: Tool parameters
143
+
144
+ Returns:
145
+ Tool execution result
146
+ """
147
+ tool = self.tools.get(tool_name)
148
+ if not tool:
149
+ raise ValueError(f"Tool '{tool_name}' not found")
150
+
151
+ # Add context to parameters only if function accepts it
152
+ import inspect
153
+
154
+ if tool.async_function:
155
+ sig = inspect.signature(tool.async_function)
156
+ else:
157
+ sig = inspect.signature(tool.function)
158
+
159
+ if "_context" in sig.parameters:
160
+ params = {**parameters, "_context": self._context}
161
+ else:
162
+ params = parameters
163
+
164
+ if tool.async_function:
165
+ return await tool.async_function(**params)
166
+ elif tool.function:
167
+ # Run sync function in executor
168
+ loop = asyncio.get_event_loop()
169
+ from functools import partial
170
+
171
+ func = partial(tool.function, **params)
172
+ return await loop.run_in_executor(None, func)
173
+ else:
174
+ raise ValueError(f"Tool '{tool_name}' has no implementation")
175
+
176
+ def execute_tool_sync(self, tool_name: str, parameters: Dict[str, Any]) -> Any:
177
+ """Execute a tool synchronously.
178
+
179
+ Args:
180
+ tool_name: Name of the tool to execute
181
+ parameters: Tool parameters
182
+
183
+ Returns:
184
+ Tool execution result
185
+ """
186
+ tool = self.tools.get(tool_name)
187
+ if not tool:
188
+ raise ValueError(f"Tool '{tool_name}' not found")
189
+
190
+ if tool.function:
191
+ # Check if function accepts _context parameter
192
+ import inspect
193
+
194
+ sig = inspect.signature(tool.function)
195
+ if "_context" in sig.parameters:
196
+ params = {**parameters, "_context": self._context}
197
+ else:
198
+ params = parameters
199
+ return tool.function(**params)
200
+ else:
201
+ raise ValueError(f"Tool '{tool_name}' requires async execution")
202
+
203
+ def get_resource(self, resource_name: str) -> Optional[MCPResource]:
204
+ """Get a resource by name.
205
+
206
+ Args:
207
+ resource_name: Resource name
208
+
209
+ Returns:
210
+ Resource if found
211
+ """
212
+ return self.resources.get(resource_name)
213
+
214
+ def set_context(self, key: str, value: Any):
215
+ """Set context value.
216
+
217
+ Args:
218
+ key: Context key
219
+ value: Context value
220
+ """
221
+ self._context[key] = value
222
+
223
+ def get_context(self, key: str) -> Any:
224
+ """Get context value.
225
+
226
+ Args:
227
+ key: Context key
228
+
229
+ Returns:
230
+ Context value
231
+ """
232
+ return self._context.get(key)
233
+
234
+ def list_tools(self) -> List[Dict[str, Any]]:
235
+ """List all available tools.
236
+
237
+ Returns:
238
+ List of tool definitions
239
+ """
240
+ return [
241
+ {
242
+ "name": tool.name,
243
+ "description": tool.description,
244
+ "parameters": tool.parameters,
245
+ }
246
+ for tool in self.tools.values()
247
+ ]
248
+
249
+ def list_resources(self) -> List[Dict[str, Any]]:
250
+ """List all available resources.
251
+
252
+ Returns:
253
+ List of resource definitions
254
+ """
255
+ return [
256
+ {
257
+ "name": resource.name,
258
+ "uri": resource.uri,
259
+ "description": resource.description,
260
+ "mime_type": resource.mime_type,
261
+ }
262
+ for resource in self.resources.values()
263
+ ]
264
+
265
+ def to_mcp_protocol(self) -> Dict[str, Any]:
266
+ """Convert to MCP protocol format.
267
+
268
+ Returns:
269
+ MCP server definition
270
+ """
271
+ return {
272
+ "name": self.name,
273
+ "description": self.description,
274
+ "capabilities": self.capabilities,
275
+ "tools": self.list_tools(),
276
+ "resources": self.list_resources(),
277
+ }
278
+
279
+
280
+ class MCPToolNode(AsyncNode):
281
+ """Node that executes MCP tools within a workflow.
282
+
283
+ This node allows workflows to use tools provided by MCP servers,
284
+ bridging the gap between workflow automation and AI-powered tools.
285
+
286
+ Example:
287
+ Using an MCP tool in a workflow:
288
+
289
+ >>> # Create node for specific tool
290
+ >>> search_node = MCPToolNode(
291
+ ... mcp_server="tools",
292
+ ... tool_name="web_search"
293
+ ... )
294
+ >>> # Add to workflow
295
+ >>> # workflow.add_node("search", search_node) # doctest: +SKIP
296
+ >>> # Execute with parameters
297
+ >>> # result = workflow.run({"search": {"query": "Kailash SDK documentation"}}) # doctest: +SKIP
298
+ """
299
+
300
+ def __init__(
301
+ self, mcp_server: str, tool_name: str, parameter_mapping: Dict[str, str] = None
302
+ ):
303
+ """Initialize MCP tool node.
304
+
305
+ Args:
306
+ mcp_server: Name of the MCP server
307
+ tool_name: Name of the tool to execute
308
+ parameter_mapping: Map input keys to tool parameters
309
+ """
310
+ super().__init__()
311
+ self.mcp_server = mcp_server
312
+ self.tool_name = tool_name
313
+ self.parameter_mapping = parameter_mapping or {}
314
+ self._mcp_integration: Optional[MCPIntegration] = None
315
+
316
+ def set_mcp_integration(self, mcp: MCPIntegration):
317
+ """Set the MCP integration instance."""
318
+ self._mcp_integration = mcp
319
+
320
+ def get_parameters(self) -> Dict[str, Any]:
321
+ """Get node parameters.
322
+
323
+ Returns:
324
+ Dictionary of parameters
325
+ """
326
+ # For MCP tools, parameters are dynamic based on the tool
327
+ return {}
328
+
329
+ def validate_inputs(self, **kwargs) -> Dict[str, Any]:
330
+ """Validate runtime inputs.
331
+
332
+ For MCPToolNode, we accept any inputs since the parameters
333
+ are dynamic based on the MCP tool being used.
334
+
335
+ Args:
336
+ **kwargs: Runtime inputs
337
+
338
+ Returns:
339
+ All inputs as-is
340
+ """
341
+ # For MCP tools, pass through all inputs without validation
342
+ # The actual validation happens in the MCP tool itself
343
+ return kwargs
344
+
345
+ def run(self, **kwargs) -> Any:
346
+ """Run the node synchronously.
347
+
348
+ Args:
349
+ **kwargs: Input parameters
350
+
351
+ Returns:
352
+ Execution result
353
+ """
354
+ if not self._mcp_integration:
355
+ raise RuntimeError("MCP integration not set")
356
+
357
+ # Map parameters if needed
358
+ tool_params = {}
359
+ for input_key, tool_key in self.parameter_mapping.items():
360
+ if input_key in kwargs:
361
+ tool_params[tool_key] = kwargs[input_key]
362
+
363
+ # Add unmapped parameters
364
+ for key, value in kwargs.items():
365
+ if key not in self.parameter_mapping:
366
+ tool_params[key] = value
367
+
368
+ # Use synchronous execution
369
+ result = self._mcp_integration.execute_tool_sync(self.tool_name, tool_params)
370
+
371
+ # Ensure result is wrapped in a dict for consistency
372
+ if not isinstance(result, dict):
373
+ return {"result": result}
374
+ return result
375
+
376
+ async def async_run(self, **kwargs) -> Dict[str, Any]:
377
+ """Run the node asynchronously.
378
+
379
+ Args:
380
+ **kwargs: Input parameters
381
+
382
+ Returns:
383
+ Dictionary of outputs
384
+ """
385
+ if not self._mcp_integration:
386
+ raise RuntimeError("MCP integration not set")
387
+
388
+ # Map parameters if needed
389
+ tool_params = {}
390
+ for input_key, tool_key in self.parameter_mapping.items():
391
+ if input_key in kwargs:
392
+ tool_params[tool_key] = kwargs[input_key]
393
+
394
+ # Add unmapped parameters
395
+ for key, value in kwargs.items():
396
+ if key not in self.parameter_mapping:
397
+ tool_params[key] = value
398
+
399
+ # Execute tool asynchronously
400
+ result = await self._mcp_integration.execute_tool(self.tool_name, tool_params)
401
+
402
+ # Ensure result is wrapped in a dict for consistency
403
+ if not isinstance(result, dict):
404
+ return {"result": result}
405
+ return result
406
+
407
+
408
+ def create_example_mcp_server() -> MCPIntegration:
409
+ """Create an example MCP server with common tools."""
410
+
411
+ mcp = MCPIntegration("example_tools", "Example MCP server with utility tools")
412
+
413
+ # Add search tool
414
+ async def web_search(query: str, max_results: int = 10, **kwargs):
415
+ """Simulate web search."""
416
+ return {
417
+ "query": query,
418
+ "results": [
419
+ {"title": f"Result {i}", "url": f"https://example.com/{i}"}
420
+ for i in range(max_results)
421
+ ],
422
+ }
423
+
424
+ mcp.add_tool(
425
+ "web_search",
426
+ web_search,
427
+ "Search the web",
428
+ {
429
+ "query": {"type": "string", "required": True},
430
+ "max_results": {"type": "integer", "default": 10},
431
+ },
432
+ )
433
+
434
+ # Add calculator tool
435
+ def calculate(expression: str, **kwargs):
436
+ """Evaluate mathematical expression."""
437
+ try:
438
+ # Safe evaluation of mathematical expressions
439
+ import ast
440
+ import operator as op
441
+
442
+ # Operators would be used for safe eval implementation
443
+ # Currently using simple eval for the expression
444
+ _ = {
445
+ ast.Add: op.add,
446
+ ast.Sub: op.sub,
447
+ ast.Mult: op.mul,
448
+ ast.Div: op.truediv,
449
+ ast.Pow: op.pow,
450
+ }
451
+
452
+ def eval_expr(expr):
453
+ return eval(expr, {"__builtins__": {}}, {})
454
+
455
+ result = eval_expr(expression)
456
+ return {"expression": expression, "result": result}
457
+ except Exception as e:
458
+ return {"error": str(e)}
459
+
460
+ mcp.add_tool(
461
+ "calculate",
462
+ calculate,
463
+ "Evaluate mathematical expressions",
464
+ {"expression": {"type": "string", "required": True}},
465
+ )
466
+
467
+ # Add resources
468
+ mcp.add_resource(
469
+ "documentation", "https://docs.kailash.io", "Kailash SDK documentation"
470
+ )
471
+
472
+ mcp.add_resource(
473
+ "examples",
474
+ "https://github.com/kailash/examples",
475
+ "Example workflows and patterns",
476
+ )
477
+
478
+ return mcp