kailash 0.1.3__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.
@@ -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
@@ -116,7 +116,7 @@ class WorkflowAPI:
116
116
  """Setup API routes dynamically based on workflow."""
117
117
 
118
118
  # Main execution endpoint
119
- @self.app.post("/execute", response_model=WorkflowResponse)
119
+ @self.app.post("/execute")
120
120
  async def execute_workflow(
121
121
  request: WorkflowRequest, background_tasks: BackgroundTasks
122
122
  ):
@@ -143,18 +143,34 @@ class WorkflowAPI:
143
143
  @self.app.get("/workflow/info")
144
144
  async def get_workflow_info():
145
145
  """Get workflow metadata and structure."""
146
- graph_data = self.workflow_graph
146
+ workflow = self.workflow_graph
147
+
148
+ # Get node information
149
+ nodes = []
150
+ for node_id, node_instance in workflow.nodes.items():
151
+ nodes.append({"id": node_id, "type": node_instance.node_type})
152
+
153
+ # Get edge information
154
+ edges = []
155
+ for conn in workflow.connections:
156
+ edges.append(
157
+ {
158
+ "source": conn.source_node,
159
+ "target": conn.target_node,
160
+ "source_output": conn.source_output,
161
+ "target_input": conn.target_input,
162
+ }
163
+ )
164
+
147
165
  return {
148
- "id": self.workflow_id,
149
- "version": self.version,
150
- "nodes": list(graph_data.nodes()),
151
- "edges": list(graph_data.edges()),
152
- "input_nodes": [
153
- n for n in graph_data.nodes() if graph_data.in_degree(n) == 0
154
- ],
155
- "output_nodes": [
156
- n for n in graph_data.nodes() if graph_data.out_degree(n) == 0
157
- ],
166
+ "workflow_id": workflow.workflow_id,
167
+ "name": workflow.name,
168
+ "description": workflow.description,
169
+ "version": workflow.version,
170
+ "nodes": nodes,
171
+ "edges": edges,
172
+ "node_count": len(nodes),
173
+ "edge_count": len(edges),
158
174
  }
159
175
 
160
176
  # Health check
@@ -179,7 +195,7 @@ class WorkflowAPI:
179
195
 
180
196
  # Execute workflow with inputs
181
197
  results = await asyncio.to_thread(
182
- self.runtime.execute, self.workflow_graph, request.inputs
198
+ self.runtime.execute, self.workflow_graph, parameters=request.inputs
183
199
  )
184
200
 
185
201
  # Handle tuple return from runtime
@@ -13,8 +13,8 @@ from .ai_providers import (
13
13
  get_available_providers,
14
14
  get_provider,
15
15
  )
16
- from .embedding_generator import EmbeddingGenerator
17
- from .llm_agent import LLMAgent
16
+ from .embedding_generator import EmbeddingGeneratorNode
17
+ from .llm_agent import LLMAgentNode
18
18
  from .models import (
19
19
  ModelPredictor,
20
20
  NamedEntityRecognizer,
@@ -30,9 +30,9 @@ __all__ = [
30
30
  "RetrievalAgent",
31
31
  "FunctionCallingAgent",
32
32
  "PlanningAgent",
33
- "LLMAgent",
33
+ "LLMAgentNode",
34
34
  # Embedding and Vector Operations
35
- "EmbeddingGenerator",
35
+ "EmbeddingGeneratorNode",
36
36
  # Provider Infrastructure
37
37
  "LLMProvider",
38
38
  "OllamaProvider",
@@ -333,7 +333,7 @@ class PlanningAgent(Node):
333
333
  # Data processing workflow
334
334
  potential_steps = [
335
335
  {
336
- "tool": "CSVReader",
336
+ "tool": "CSVReaderNode",
337
337
  "description": "Read input data",
338
338
  "parameters": {"file_path": "input.csv"},
339
339
  },
@@ -348,7 +348,7 @@ class PlanningAgent(Node):
348
348
  "parameters": {"group_by": "category", "operation": "sum"},
349
349
  },
350
350
  {
351
- "tool": "CSVWriter",
351
+ "tool": "CSVWriterNode",
352
352
  "description": "Write results",
353
353
  "parameters": {"file_path": "output.csv"},
354
354
  },
@@ -357,7 +357,7 @@ class PlanningAgent(Node):
357
357
  # Text analysis workflow
358
358
  potential_steps = [
359
359
  {
360
- "tool": "TextReader",
360
+ "tool": "TextReaderNode",
361
361
  "description": "Read text data",
362
362
  "parameters": {"file_path": "text.txt"},
363
363
  },
@@ -372,7 +372,7 @@ class PlanningAgent(Node):
372
372
  "parameters": {"max_length": 200},
373
373
  },
374
374
  {
375
- "tool": "JSONWriter",
375
+ "tool": "JSONWriterNode",
376
376
  "description": "Save analysis results",
377
377
  "parameters": {"file_path": "analysis.json"},
378
378
  },
@@ -1159,18 +1159,17 @@ def get_provider(
1159
1159
  ValueError: If the provider name is not recognized or doesn't support the requested type.
1160
1160
 
1161
1161
  Examples:
1162
-
1163
- Get any provider::
1164
-
1165
- provider = get_provider("openai")
1166
- if provider.supports_chat():
1167
- # Use for chat
1168
- if provider.supports_embeddings():
1169
- # Use for embeddings
1170
-
1171
- Get chat-only provider:
1172
-
1173
- chat_provider = get_provider("anthropic", "chat")
1162
+ >>> # Get any provider
1163
+ >>> provider = get_provider("openai")
1164
+ >>> if provider.supports_chat():
1165
+ ... # Use for chat
1166
+ ... pass
1167
+ >>> if provider.supports_embeddings():
1168
+ ... # Use for embeddings
1169
+ ... pass
1170
+
1171
+ >>> # Get chat-only provider
1172
+ >>> chat_provider = get_provider("anthropic", "chat")
1174
1173
  response = chat_provider.chat(messages, model="claude-3-sonnet")
1175
1174
 
1176
1175
  Get embedding-only provider:
@@ -1223,18 +1222,15 @@ def get_available_providers(
1223
1222
  Dict mapping provider names to their availability and capabilities.
1224
1223
 
1225
1224
  Examples:
1225
+ >>> # Get all providers
1226
+ >>> all_providers = get_available_providers()
1227
+ >>> for name, info in all_providers.items():
1228
+ ... print(f"{name}: Available={info['available']}, Chat={info['chat']}, Embeddings={info['embeddings']}")
1226
1229
 
1227
- Get all providers::
1228
-
1229
- all_providers = get_available_providers()
1230
- for name, info in all_providers.items():
1231
- print(f"{name}: Available={info['available']}, Chat={info['chat']}, Embeddings={info['embeddings']}")
1232
-
1233
- Get only chat providers:
1234
-
1235
- chat_providers = get_available_providers("chat")
1230
+ >>> # Get only chat providers
1231
+ >>> chat_providers = get_available_providers("chat")
1236
1232
 
1237
- Get only embedding providers:
1233
+ >>> # Get only embedding providers
1238
1234
 
1239
1235
  embed_providers = get_available_providers("embeddings")
1240
1236
  """