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.
Files changed (63) hide show
  1. kailash/__init__.py +35 -5
  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/mcp/enhanced_server.py +22 -16
  13. kailash/nexus/__init__.py +21 -0
  14. kailash/nexus/factory.py +413 -0
  15. kailash/nexus/gateway.py +545 -0
  16. kailash/nodes/__init__.py +2 -0
  17. kailash/nodes/ai/iterative_llm_agent.py +988 -17
  18. kailash/nodes/ai/llm_agent.py +29 -9
  19. kailash/nodes/api/__init__.py +2 -2
  20. kailash/nodes/api/monitoring.py +1 -1
  21. kailash/nodes/base_async.py +54 -14
  22. kailash/nodes/code/async_python.py +1 -1
  23. kailash/nodes/data/bulk_operations.py +939 -0
  24. kailash/nodes/data/query_builder.py +373 -0
  25. kailash/nodes/data/query_cache.py +512 -0
  26. kailash/nodes/monitoring/__init__.py +10 -0
  27. kailash/nodes/monitoring/deadlock_detector.py +964 -0
  28. kailash/nodes/monitoring/performance_anomaly.py +1078 -0
  29. kailash/nodes/monitoring/race_condition_detector.py +1151 -0
  30. kailash/nodes/monitoring/transaction_metrics.py +790 -0
  31. kailash/nodes/monitoring/transaction_monitor.py +931 -0
  32. kailash/nodes/system/__init__.py +17 -0
  33. kailash/nodes/system/command_parser.py +820 -0
  34. kailash/nodes/transaction/__init__.py +48 -0
  35. kailash/nodes/transaction/distributed_transaction_manager.py +983 -0
  36. kailash/nodes/transaction/saga_coordinator.py +652 -0
  37. kailash/nodes/transaction/saga_state_storage.py +411 -0
  38. kailash/nodes/transaction/saga_step.py +467 -0
  39. kailash/nodes/transaction/transaction_context.py +756 -0
  40. kailash/nodes/transaction/two_phase_commit.py +978 -0
  41. kailash/nodes/transform/processors.py +17 -1
  42. kailash/nodes/validation/__init__.py +21 -0
  43. kailash/nodes/validation/test_executor.py +532 -0
  44. kailash/nodes/validation/validation_nodes.py +447 -0
  45. kailash/resources/factory.py +1 -1
  46. kailash/runtime/async_local.py +84 -21
  47. kailash/runtime/local.py +21 -2
  48. kailash/runtime/parameter_injector.py +187 -31
  49. kailash/security.py +16 -1
  50. kailash/servers/__init__.py +32 -0
  51. kailash/servers/durable_workflow_server.py +430 -0
  52. kailash/servers/enterprise_workflow_server.py +466 -0
  53. kailash/servers/gateway.py +183 -0
  54. kailash/servers/workflow_server.py +290 -0
  55. kailash/utils/data_validation.py +192 -0
  56. kailash/workflow/builder.py +291 -12
  57. kailash/workflow/validation.py +144 -8
  58. {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/METADATA +1 -1
  59. {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/RECORD +63 -25
  60. {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/WHEEL +0 -0
  61. {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/entry_points.txt +0 -0
  62. {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/licenses/LICENSE +0 -0
  63. {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,290 @@
1
+ """Basic workflow server implementation.
2
+
3
+ This module provides WorkflowServer - a renamed and improved version of
4
+ WorkflowAPIGateway with clearer naming and better organization.
5
+ """
6
+
7
+ import logging
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ from contextlib import asynccontextmanager
10
+ from typing import Any
11
+
12
+ from fastapi import FastAPI, WebSocket
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from pydantic import BaseModel, Field
15
+
16
+ from ..api.workflow_api import WorkflowAPI
17
+ from ..workflow import Workflow
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class WorkflowRegistration(BaseModel):
23
+ """Registration details for a workflow."""
24
+
25
+ model_config = {"arbitrary_types_allowed": True}
26
+
27
+ name: str
28
+ type: str = Field(description="embedded or proxied")
29
+ workflow: Workflow | None = None
30
+ proxy_url: str | None = None
31
+ health_check: str | None = None
32
+ description: str | None = None
33
+ version: str = "1.0.0"
34
+ tags: list[str] = Field(default_factory=list)
35
+
36
+
37
+ class WorkflowServer:
38
+ """Basic workflow server for hosting multiple Kailash workflows.
39
+
40
+ This server provides:
41
+ - Multi-workflow hosting with dynamic registration
42
+ - REST API endpoints for workflow execution
43
+ - WebSocket support for real-time updates
44
+ - MCP server integration
45
+ - Health monitoring
46
+ - CORS support
47
+
48
+ This is the base server class. For production deployments, consider
49
+ using EnterpriseWorkflowServer which includes durability, security,
50
+ and monitoring features.
51
+
52
+ Attributes:
53
+ app: FastAPI application instance
54
+ workflows: Registry of all registered workflows
55
+ executor: Thread pool for synchronous execution
56
+ mcp_servers: Registry of MCP servers
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ title: str = "Kailash Workflow Server",
62
+ description: str = "Multi-workflow hosting server",
63
+ version: str = "1.0.0",
64
+ max_workers: int = 10,
65
+ cors_origins: list[str] = None,
66
+ **kwargs,
67
+ ):
68
+ """Initialize the workflow server.
69
+
70
+ Args:
71
+ title: Server title for documentation
72
+ description: Server description
73
+ version: Server version
74
+ max_workers: Maximum thread pool workers
75
+ cors_origins: Allowed CORS origins
76
+ """
77
+ self.workflows: dict[str, WorkflowRegistration] = {}
78
+ self.mcp_servers: dict[str, Any] = {}
79
+ self.executor = ThreadPoolExecutor(max_workers=max_workers)
80
+
81
+ # Create FastAPI app with lifespan
82
+ @asynccontextmanager
83
+ async def lifespan(app: FastAPI):
84
+ # Startup
85
+ logger.info(f"Starting {title} v{version}")
86
+ yield
87
+ # Shutdown
88
+ logger.info("Shutting down workflow server")
89
+ self.executor.shutdown(wait=True)
90
+
91
+ self.app = FastAPI(
92
+ title=title, description=description, version=version, lifespan=lifespan
93
+ )
94
+
95
+ # Add CORS middleware
96
+ if cors_origins:
97
+ self.app.add_middleware(
98
+ CORSMiddleware,
99
+ allow_origins=cors_origins,
100
+ allow_credentials=True,
101
+ allow_methods=["*"],
102
+ allow_headers=["*"],
103
+ )
104
+
105
+ # Register root endpoints
106
+ self._register_root_endpoints()
107
+
108
+ def _register_root_endpoints(self):
109
+ """Register server-level endpoints."""
110
+
111
+ @self.app.get("/")
112
+ async def root():
113
+ """Server information."""
114
+ return {
115
+ "name": self.app.title,
116
+ "version": self.app.version,
117
+ "workflows": list(self.workflows.keys()),
118
+ "mcp_servers": list(self.mcp_servers.keys()),
119
+ "type": "workflow_server",
120
+ }
121
+
122
+ @self.app.get("/workflows")
123
+ async def list_workflows():
124
+ """List all registered workflows."""
125
+ return {
126
+ name: {
127
+ "type": reg.type,
128
+ "description": reg.description,
129
+ "version": reg.version,
130
+ "tags": reg.tags,
131
+ "endpoints": self._get_workflow_endpoints(name),
132
+ }
133
+ for name, reg in self.workflows.items()
134
+ }
135
+
136
+ @self.app.get("/health")
137
+ async def health_check():
138
+ """Server health check."""
139
+ health_status = {
140
+ "status": "healthy",
141
+ "server_type": "workflow_server",
142
+ "workflows": {},
143
+ "mcp_servers": {},
144
+ }
145
+
146
+ # Check workflow health
147
+ for name, reg in self.workflows.items():
148
+ if reg.type == "embedded":
149
+ health_status["workflows"][name] = "healthy"
150
+ else:
151
+ # TODO: Implement proxy health check
152
+ health_status["workflows"][name] = "unknown"
153
+
154
+ # Check MCP server health
155
+ for name, server in self.mcp_servers.items():
156
+ # TODO: Implement MCP health check
157
+ health_status["mcp_servers"][name] = "unknown"
158
+
159
+ return health_status
160
+
161
+ @self.app.websocket("/ws")
162
+ async def websocket_endpoint(websocket: WebSocket):
163
+ """WebSocket for real-time updates."""
164
+ await websocket.accept()
165
+ try:
166
+ while True:
167
+ # Basic WebSocket echo - subclasses can override
168
+ data = await websocket.receive_text()
169
+ await websocket.send_text(f"Echo: {data}")
170
+ except Exception as e:
171
+ logger.error(f"WebSocket error: {e}")
172
+ finally:
173
+ await websocket.close()
174
+
175
+ def register_workflow(
176
+ self,
177
+ name: str,
178
+ workflow: Workflow,
179
+ description: str = None,
180
+ tags: list[str] = None,
181
+ ):
182
+ """Register a workflow with the server.
183
+
184
+ Args:
185
+ name: Unique workflow identifier
186
+ workflow: Workflow instance to register
187
+ description: Optional workflow description
188
+ tags: Optional tags for categorization
189
+ """
190
+ if name in self.workflows:
191
+ raise ValueError(f"Workflow '{name}' already registered")
192
+
193
+ # Create workflow registration
194
+ registration = WorkflowRegistration(
195
+ name=name,
196
+ type="embedded",
197
+ workflow=workflow,
198
+ description=description or f"Workflow: {name}",
199
+ tags=tags or [],
200
+ )
201
+
202
+ self.workflows[name] = registration
203
+
204
+ # Create workflow API wrapper
205
+ workflow_api = WorkflowAPI(workflow)
206
+
207
+ # Register workflow endpoints with prefix
208
+ prefix = f"/workflows/{name}"
209
+ self.app.mount(prefix, workflow_api.app)
210
+
211
+ logger.info(f"Registered workflow '{name}' at {prefix}")
212
+
213
+ def register_mcp_server(self, name: str, mcp_server: Any):
214
+ """Register an MCP server with the workflow server.
215
+
216
+ Args:
217
+ name: Unique MCP server identifier
218
+ mcp_server: MCP server instance
219
+ """
220
+ if name in self.mcp_servers:
221
+ raise ValueError(f"MCP server '{name}' already registered")
222
+
223
+ self.mcp_servers[name] = mcp_server
224
+
225
+ # Mount MCP server endpoints
226
+ mcp_prefix = f"/mcp/{name}"
227
+ # TODO: Implement MCP mounting logic
228
+
229
+ logger.info(f"Registered MCP server '{name}' at {mcp_prefix}")
230
+
231
+ def proxy_workflow(
232
+ self,
233
+ name: str,
234
+ proxy_url: str,
235
+ health_check: str = "/health",
236
+ description: str = None,
237
+ tags: list[str] = None,
238
+ ):
239
+ """Register a proxied workflow running on another server.
240
+
241
+ Args:
242
+ name: Unique workflow identifier
243
+ proxy_url: Base URL of the proxied workflow
244
+ health_check: Health check endpoint path
245
+ description: Optional workflow description
246
+ tags: Optional tags for categorization
247
+ """
248
+ if name in self.workflows:
249
+ raise ValueError(f"Workflow '{name}' already registered")
250
+
251
+ # Create proxied workflow registration
252
+ registration = WorkflowRegistration(
253
+ name=name,
254
+ type="proxied",
255
+ proxy_url=proxy_url,
256
+ health_check=health_check,
257
+ description=description or f"Proxied workflow: {name}",
258
+ tags=tags or [],
259
+ )
260
+
261
+ self.workflows[name] = registration
262
+
263
+ # TODO: Implement proxy endpoint creation
264
+ logger.info(f"Registered proxied workflow '{name}' -> {proxy_url}")
265
+
266
+ def _get_workflow_endpoints(self, name: str) -> list[str]:
267
+ """Get available endpoints for a workflow."""
268
+ base = f"/workflows/{name}"
269
+ return [
270
+ f"{base}/execute",
271
+ f"{base}/status",
272
+ f"{base}/schema",
273
+ f"{base}/docs",
274
+ ]
275
+
276
+ def run(self, host: str = "0.0.0.0", port: int = 8000, **kwargs):
277
+ """Run the workflow server.
278
+
279
+ Args:
280
+ host: Host address to bind to
281
+ port: Port to listen on
282
+ **kwargs: Additional arguments passed to uvicorn
283
+ """
284
+ import uvicorn
285
+
286
+ uvicorn.run(self.app, host=host, port=port, **kwargs)
287
+
288
+ def execute(self, **kwargs):
289
+ """Execute the server (alias for run)."""
290
+ self.run(**kwargs)
@@ -0,0 +1,192 @@
1
+ """Data validation and type consistency utilities for workflow execution."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Union
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class DataTypeValidator:
10
+ """Validates and fixes data type inconsistencies in workflow execution."""
11
+
12
+ @staticmethod
13
+ def validate_node_output(node_id: str, output: Dict[str, Any]) -> Dict[str, Any]:
14
+ """Validate and fix node output to ensure consistent data types.
15
+
16
+ Args:
17
+ node_id: ID of the node producing the output
18
+ output: Raw output from the node
19
+
20
+ Returns:
21
+ Validated and potentially fixed output
22
+ """
23
+ if not isinstance(output, dict):
24
+ logger.warning(
25
+ f"Node '{node_id}' output should be a dict, got {type(output)}. Wrapping in result key."
26
+ )
27
+ return {"result": output}
28
+
29
+ validated_output = {}
30
+
31
+ for key, value in output.items():
32
+ validated_value = DataTypeValidator._validate_value(node_id, key, value)
33
+ validated_output[key] = validated_value
34
+
35
+ return validated_output
36
+
37
+ @staticmethod
38
+ def _validate_value(node_id: str, key: str, value: Any) -> Any:
39
+ """Validate a single value and fix common type issues.
40
+
41
+ Args:
42
+ node_id: ID of the node producing the value
43
+ key: Key name for the value
44
+ value: The value to validate
45
+
46
+ Returns:
47
+ Validated value
48
+ """
49
+ # Common bug: Dictionary gets converted to list of keys
50
+ if isinstance(value, list) and key == "result":
51
+ # Check if this looks like dict keys
52
+ if all(isinstance(item, str) for item in value):
53
+ logger.warning(
54
+ f"Node '{node_id}' output '{key}' appears to be dict keys converted to list: {value}. "
55
+ "This is a known bug in some node implementations."
56
+ )
57
+ # We can't recover the original dict, so wrap the list properly
58
+ return value
59
+
60
+ # Ensure string data is not accidentally indexed as dict
61
+ if isinstance(value, str):
62
+ return value
63
+
64
+ # Validate dict structure
65
+ if isinstance(value, dict):
66
+ # Recursively validate nested dicts
67
+ validated_dict = {}
68
+ for subkey, subvalue in value.items():
69
+ validated_dict[subkey] = DataTypeValidator._validate_value(
70
+ node_id, f"{key}.{subkey}", subvalue
71
+ )
72
+ return validated_dict
73
+
74
+ # Validate list structure
75
+ if isinstance(value, list):
76
+ # Ensure list elements are consistently typed
77
+ if len(value) > 0:
78
+ first_type = type(value[0])
79
+ inconsistent_types = [
80
+ i for i, item in enumerate(value) if type(item) is not first_type
81
+ ]
82
+ if inconsistent_types:
83
+ logger.warning(
84
+ f"Node '{node_id}' output '{key}' has inconsistent list element types. "
85
+ f"First type: {first_type}, inconsistent indices: {inconsistent_types[:5]}"
86
+ )
87
+
88
+ return value
89
+
90
+ @staticmethod
91
+ def validate_node_input(node_id: str, inputs: Dict[str, Any]) -> Dict[str, Any]:
92
+ """Validate node inputs before execution.
93
+
94
+ Args:
95
+ node_id: ID of the node receiving the inputs
96
+ inputs: Input parameters for the node
97
+
98
+ Returns:
99
+ Validated inputs
100
+ """
101
+ if not isinstance(inputs, dict):
102
+ logger.error(f"Node '{node_id}' inputs must be a dict, got {type(inputs)}")
103
+ return {}
104
+
105
+ validated_inputs = {}
106
+
107
+ for key, value in inputs.items():
108
+ # Handle common data mapping issues
109
+ if key == "data" and isinstance(value, list):
110
+ # Check if this is the dict-to-keys bug
111
+ if all(isinstance(item, str) for item in value):
112
+ logger.warning(
113
+ f"Node '{node_id}' received list of strings for 'data' parameter: {value}. "
114
+ "This may be due to a dict-to-keys conversion bug in upstream node."
115
+ )
116
+
117
+ validated_inputs[key] = value
118
+
119
+ return validated_inputs
120
+
121
+ @staticmethod
122
+ def fix_string_indexing_error(data: Any, error_context: str = "") -> Any:
123
+ """Fix common 'string indices must be integers' errors.
124
+
125
+ Args:
126
+ data: Data that caused the error
127
+ error_context: Context information about the error
128
+
129
+ Returns:
130
+ Fixed data or None if unfixable
131
+ """
132
+ if isinstance(data, str):
133
+ logger.warning(
134
+ f"Attempting to index string as dict{' in ' + error_context if error_context else ''}. "
135
+ f"String value: '{data[:100]}...'"
136
+ if len(data) > 100
137
+ else f"String value: '{data}'"
138
+ )
139
+ return None
140
+
141
+ if isinstance(data, list) and all(isinstance(item, str) for item in data):
142
+ logger.warning(
143
+ f"Data appears to be list of dict keys{' in ' + error_context if error_context else ''}. "
144
+ f"Keys: {data}. Cannot recover original dict structure."
145
+ )
146
+ return None
147
+
148
+ return data
149
+
150
+ @staticmethod
151
+ def create_error_recovery_wrapper(
152
+ original_data: Any, fallback_data: Any = None
153
+ ) -> Dict[str, Any]:
154
+ """Create a recovery wrapper for problematic data.
155
+
156
+ Args:
157
+ original_data: The problematic data
158
+ fallback_data: Fallback data to use if original is unusable
159
+
160
+ Returns:
161
+ Recovery wrapper dict
162
+ """
163
+ return {
164
+ "data": fallback_data if fallback_data is not None else {},
165
+ "original_data": original_data,
166
+ "data_type_error": True,
167
+ "error_message": f"Data type conversion error. Original type: {type(original_data)}",
168
+ }
169
+
170
+
171
+ def validate_workflow_data_flow(workflow_results: Dict[str, Any]) -> Dict[str, Any]:
172
+ """Validate entire workflow result data flow for consistency.
173
+
174
+ Args:
175
+ workflow_results: Results from workflow execution
176
+
177
+ Returns:
178
+ Validated workflow results
179
+ """
180
+ validated_results = {}
181
+
182
+ for node_id, result in workflow_results.items():
183
+ try:
184
+ validated_result = DataTypeValidator.validate_node_output(node_id, result)
185
+ validated_results[node_id] = validated_result
186
+ except Exception as e:
187
+ logger.error(f"Data validation failed for node '{node_id}': {e}")
188
+ validated_results[node_id] = (
189
+ DataTypeValidator.create_error_recovery_wrapper(result)
190
+ )
191
+
192
+ return validated_results