kailash 0.1.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 (69) hide show
  1. kailash/__init__.py +31 -0
  2. kailash/__main__.py +11 -0
  3. kailash/cli/__init__.py +5 -0
  4. kailash/cli/commands.py +563 -0
  5. kailash/manifest.py +778 -0
  6. kailash/nodes/__init__.py +23 -0
  7. kailash/nodes/ai/__init__.py +26 -0
  8. kailash/nodes/ai/agents.py +417 -0
  9. kailash/nodes/ai/models.py +488 -0
  10. kailash/nodes/api/__init__.py +52 -0
  11. kailash/nodes/api/auth.py +567 -0
  12. kailash/nodes/api/graphql.py +480 -0
  13. kailash/nodes/api/http.py +598 -0
  14. kailash/nodes/api/rate_limiting.py +572 -0
  15. kailash/nodes/api/rest.py +665 -0
  16. kailash/nodes/base.py +1032 -0
  17. kailash/nodes/base_async.py +128 -0
  18. kailash/nodes/code/__init__.py +32 -0
  19. kailash/nodes/code/python.py +1021 -0
  20. kailash/nodes/data/__init__.py +125 -0
  21. kailash/nodes/data/readers.py +496 -0
  22. kailash/nodes/data/sharepoint_graph.py +623 -0
  23. kailash/nodes/data/sql.py +380 -0
  24. kailash/nodes/data/streaming.py +1168 -0
  25. kailash/nodes/data/vector_db.py +964 -0
  26. kailash/nodes/data/writers.py +529 -0
  27. kailash/nodes/logic/__init__.py +6 -0
  28. kailash/nodes/logic/async_operations.py +702 -0
  29. kailash/nodes/logic/operations.py +551 -0
  30. kailash/nodes/transform/__init__.py +5 -0
  31. kailash/nodes/transform/processors.py +379 -0
  32. kailash/runtime/__init__.py +6 -0
  33. kailash/runtime/async_local.py +356 -0
  34. kailash/runtime/docker.py +697 -0
  35. kailash/runtime/local.py +434 -0
  36. kailash/runtime/parallel.py +557 -0
  37. kailash/runtime/runner.py +110 -0
  38. kailash/runtime/testing.py +347 -0
  39. kailash/sdk_exceptions.py +307 -0
  40. kailash/tracking/__init__.py +7 -0
  41. kailash/tracking/manager.py +885 -0
  42. kailash/tracking/metrics_collector.py +342 -0
  43. kailash/tracking/models.py +535 -0
  44. kailash/tracking/storage/__init__.py +0 -0
  45. kailash/tracking/storage/base.py +113 -0
  46. kailash/tracking/storage/database.py +619 -0
  47. kailash/tracking/storage/filesystem.py +543 -0
  48. kailash/utils/__init__.py +0 -0
  49. kailash/utils/export.py +924 -0
  50. kailash/utils/templates.py +680 -0
  51. kailash/visualization/__init__.py +62 -0
  52. kailash/visualization/api.py +732 -0
  53. kailash/visualization/dashboard.py +951 -0
  54. kailash/visualization/performance.py +808 -0
  55. kailash/visualization/reports.py +1471 -0
  56. kailash/workflow/__init__.py +15 -0
  57. kailash/workflow/builder.py +245 -0
  58. kailash/workflow/graph.py +827 -0
  59. kailash/workflow/mermaid_visualizer.py +628 -0
  60. kailash/workflow/mock_registry.py +63 -0
  61. kailash/workflow/runner.py +302 -0
  62. kailash/workflow/state.py +238 -0
  63. kailash/workflow/visualization.py +588 -0
  64. kailash-0.1.0.dist-info/METADATA +710 -0
  65. kailash-0.1.0.dist-info/RECORD +69 -0
  66. kailash-0.1.0.dist-info/WHEEL +5 -0
  67. kailash-0.1.0.dist-info/entry_points.txt +2 -0
  68. kailash-0.1.0.dist-info/licenses/LICENSE +21 -0
  69. kailash-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,356 @@
1
+ """Asynchronous local runtime engine for executing workflows.
2
+
3
+ This module provides an asynchronous execution engine for Kailash workflows,
4
+ particularly useful for workflows with I/O-bound nodes such as API calls,
5
+ database queries, or LLM interactions.
6
+ """
7
+
8
+ import logging
9
+ from datetime import datetime, timezone
10
+ from typing import Any, Dict, Optional, Tuple
11
+
12
+ import networkx as nx
13
+
14
+ from kailash.nodes.base_async import AsyncNode
15
+ from kailash.sdk_exceptions import (
16
+ RuntimeExecutionError,
17
+ WorkflowExecutionError,
18
+ WorkflowValidationError,
19
+ )
20
+ from kailash.tracking import TaskManager, TaskStatus
21
+ from kailash.workflow.graph import Workflow
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class AsyncLocalRuntime:
27
+ """Asynchronous local execution engine for workflows.
28
+
29
+ This runtime provides asynchronous execution capabilities for workflows,
30
+ allowing for more efficient processing of I/O-bound operations and potential
31
+ parallel execution of independent nodes.
32
+
33
+ Key features:
34
+ - Support for AsyncNode.async_run() execution
35
+ - Parallel execution of independent nodes (in development)
36
+ - Task tracking and monitoring
37
+ - Detailed execution metrics
38
+
39
+ Usage:
40
+ runtime = AsyncLocalRuntime()
41
+ results = await runtime.execute(workflow, parameters={...})
42
+ """
43
+
44
+ def __init__(self, debug: bool = False, max_concurrency: int = 10):
45
+ """Initialize the async local runtime.
46
+
47
+ Args:
48
+ debug: Whether to enable debug logging
49
+ max_concurrency: Maximum number of nodes to execute concurrently
50
+ """
51
+ self.debug = debug
52
+ self.max_concurrency = max_concurrency
53
+ self.logger = logger
54
+
55
+ if debug:
56
+ self.logger.setLevel(logging.DEBUG)
57
+ else:
58
+ self.logger.setLevel(logging.INFO)
59
+
60
+ async def execute(
61
+ self,
62
+ workflow: Workflow,
63
+ task_manager: Optional[TaskManager] = None,
64
+ parameters: Optional[Dict[str, Dict[str, Any]]] = None,
65
+ ) -> Tuple[Dict[str, Any], Optional[str]]:
66
+ """Execute a workflow asynchronously.
67
+
68
+ Args:
69
+ workflow: Workflow to execute
70
+ task_manager: Optional task manager for tracking
71
+ parameters: Optional parameter overrides per node
72
+
73
+ Returns:
74
+ Tuple of (results dict, run_id)
75
+
76
+ Raises:
77
+ RuntimeExecutionError: If execution fails
78
+ WorkflowValidationError: If workflow is invalid
79
+ """
80
+ if not workflow:
81
+ raise RuntimeExecutionError("No workflow provided")
82
+
83
+ run_id = None
84
+
85
+ try:
86
+ # Validate workflow
87
+ workflow.validate()
88
+
89
+ # Initialize tracking
90
+ if task_manager:
91
+ try:
92
+ run_id = task_manager.create_run(
93
+ workflow_name=workflow.name,
94
+ metadata={
95
+ "parameters": parameters,
96
+ "debug": self.debug,
97
+ "runtime": "async_local",
98
+ },
99
+ )
100
+ except Exception as e:
101
+ self.logger.warning(f"Failed to create task run: {e}")
102
+ # Continue without tracking
103
+
104
+ # Execute workflow
105
+ results = await self._execute_workflow(
106
+ workflow=workflow,
107
+ task_manager=task_manager,
108
+ run_id=run_id,
109
+ parameters=parameters or {},
110
+ )
111
+
112
+ # Mark run as completed
113
+ if task_manager and run_id:
114
+ try:
115
+ task_manager.update_run_status(run_id, "completed")
116
+ except Exception as e:
117
+ self.logger.warning(f"Failed to update run status: {e}")
118
+
119
+ return results, run_id
120
+
121
+ except WorkflowValidationError:
122
+ # Re-raise validation errors as-is
123
+ if task_manager and run_id:
124
+ try:
125
+ task_manager.update_run_status(
126
+ run_id, "failed", error="Validation failed"
127
+ )
128
+ except Exception:
129
+ pass
130
+ raise
131
+ except Exception as e:
132
+ # Mark run as failed
133
+ if task_manager and run_id:
134
+ try:
135
+ task_manager.update_run_status(run_id, "failed", error=str(e))
136
+ except Exception:
137
+ pass
138
+
139
+ # Wrap other errors in RuntimeExecutionError
140
+ raise RuntimeExecutionError(
141
+ f"Async workflow execution failed: {type(e).__name__}: {e}"
142
+ ) from e
143
+
144
+ async def _execute_workflow(
145
+ self,
146
+ workflow: Workflow,
147
+ task_manager: Optional[TaskManager],
148
+ run_id: Optional[str],
149
+ parameters: Dict[str, Dict[str, Any]],
150
+ ) -> Dict[str, Any]:
151
+ """Execute the workflow nodes asynchronously.
152
+
153
+ Args:
154
+ workflow: Workflow to execute
155
+ task_manager: Task manager for tracking
156
+ run_id: Run ID for tracking
157
+ parameters: Parameter overrides
158
+
159
+ Returns:
160
+ Dictionary of node results
161
+
162
+ Raises:
163
+ WorkflowExecutionError: If execution fails
164
+ """
165
+ # Get execution order
166
+ try:
167
+ execution_order = list(nx.topological_sort(workflow.graph))
168
+ self.logger.info(f"Determined execution order: {execution_order}")
169
+ except nx.NetworkXError as e:
170
+ raise WorkflowExecutionError(
171
+ f"Failed to determine execution order: {e}"
172
+ ) from e
173
+
174
+ # Initialize results storage
175
+ results = {}
176
+ node_outputs = {}
177
+ failed_nodes = []
178
+
179
+ # Execute each node
180
+ for node_id in execution_order:
181
+ self.logger.info(f"Executing node: {node_id}")
182
+
183
+ # Get node instance
184
+ node_instance = workflow._node_instances.get(node_id)
185
+ if not node_instance:
186
+ raise WorkflowExecutionError(
187
+ f"Node instance '{node_id}' not found in workflow"
188
+ )
189
+
190
+ # Start task tracking
191
+ task = None
192
+ if task_manager and run_id:
193
+ try:
194
+ task = task_manager.create_task(
195
+ run_id=run_id,
196
+ node_id=node_id,
197
+ node_type=node_instance.__class__.__name__,
198
+ started_at=datetime.now(timezone.utc),
199
+ )
200
+ except Exception as e:
201
+ self.logger.warning(
202
+ f"Failed to create task for node '{node_id}': {e}"
203
+ )
204
+
205
+ try:
206
+ # Prepare inputs
207
+ inputs = self._prepare_node_inputs(
208
+ workflow=workflow,
209
+ node_id=node_id,
210
+ node_instance=node_instance,
211
+ node_outputs=node_outputs,
212
+ parameters=parameters.get(node_id, {}),
213
+ )
214
+
215
+ if self.debug:
216
+ self.logger.debug(f"Node {node_id} inputs: {inputs}")
217
+
218
+ # Update task status
219
+ if task:
220
+ task.update_status(TaskStatus.RUNNING)
221
+
222
+ # Execute node - check if it supports async execution
223
+ start_time = datetime.now(timezone.utc)
224
+
225
+ if isinstance(node_instance, AsyncNode):
226
+ # Use async execution
227
+ outputs = await node_instance.execute_async(**inputs)
228
+ else:
229
+ # Fall back to synchronous execution
230
+ outputs = node_instance.execute(**inputs)
231
+
232
+ execution_time = (
233
+ datetime.now(timezone.utc) - start_time
234
+ ).total_seconds()
235
+
236
+ # Store outputs
237
+ node_outputs[node_id] = outputs
238
+ results[node_id] = outputs
239
+
240
+ if self.debug:
241
+ self.logger.debug(f"Node {node_id} outputs: {outputs}")
242
+
243
+ # Update task status
244
+ if task:
245
+ task.update_status(
246
+ TaskStatus.COMPLETED,
247
+ result=outputs,
248
+ ended_at=datetime.now(timezone.utc),
249
+ metadata={"execution_time": execution_time},
250
+ )
251
+
252
+ self.logger.info(
253
+ f"Node {node_id} completed successfully in {execution_time:.3f}s"
254
+ )
255
+
256
+ except Exception as e:
257
+ failed_nodes.append(node_id)
258
+ self.logger.error(f"Node {node_id} failed: {e}", exc_info=self.debug)
259
+
260
+ # Update task status
261
+ if task:
262
+ task.update_status(
263
+ TaskStatus.FAILED,
264
+ error=str(e),
265
+ ended_at=datetime.now(timezone.utc),
266
+ )
267
+
268
+ # Determine if we should continue or stop
269
+ if self._should_stop_on_error(workflow, node_id):
270
+ error_msg = f"Node '{node_id}' failed: {e}"
271
+ if len(failed_nodes) > 1:
272
+ error_msg += f" (Previously failed nodes: {failed_nodes[:-1]})"
273
+
274
+ raise WorkflowExecutionError(error_msg) from e
275
+ else:
276
+ # Continue execution but record error
277
+ results[node_id] = {
278
+ "error": str(e),
279
+ "error_type": type(e).__name__,
280
+ "failed": True,
281
+ }
282
+
283
+ return results
284
+
285
+ def _prepare_node_inputs(
286
+ self,
287
+ workflow: Workflow,
288
+ node_id: str,
289
+ node_instance: Any,
290
+ node_outputs: Dict[str, Dict[str, Any]],
291
+ parameters: Dict[str, Any],
292
+ ) -> Dict[str, Any]:
293
+ """Prepare inputs for a node execution.
294
+
295
+ Args:
296
+ workflow: The workflow being executed
297
+ node_id: Current node ID
298
+ node_instance: Current node instance
299
+ node_outputs: Outputs from previously executed nodes
300
+ parameters: Parameter overrides
301
+
302
+ Returns:
303
+ Dictionary of inputs for the node
304
+
305
+ Raises:
306
+ WorkflowExecutionError: If input preparation fails
307
+ """
308
+ inputs = {}
309
+
310
+ # Start with node configuration
311
+ inputs.update(node_instance.config)
312
+
313
+ # Add connected inputs from other nodes
314
+ for edge in workflow.graph.in_edges(node_id, data=True):
315
+ source_node_id = edge[0]
316
+ mapping = edge[2].get("mapping", {})
317
+
318
+ if source_node_id in node_outputs:
319
+ source_outputs = node_outputs[source_node_id]
320
+
321
+ # Check if the source node failed
322
+ if isinstance(source_outputs, dict) and source_outputs.get("failed"):
323
+ raise WorkflowExecutionError(
324
+ f"Cannot use outputs from failed node '{source_node_id}'"
325
+ )
326
+
327
+ for source_key, target_key in mapping.items():
328
+ if source_key in source_outputs:
329
+ inputs[target_key] = source_outputs[source_key]
330
+ else:
331
+ self.logger.warning(
332
+ f"Source output '{source_key}' not found in node '{source_node_id}'. "
333
+ f"Available outputs: {list(source_outputs.keys())}"
334
+ )
335
+
336
+ # Apply parameter overrides
337
+ inputs.update(parameters)
338
+
339
+ return inputs
340
+
341
+ def _should_stop_on_error(self, workflow: Workflow, node_id: str) -> bool:
342
+ """Determine if execution should stop when a node fails.
343
+
344
+ Args:
345
+ workflow: The workflow being executed
346
+ node_id: Failed node ID
347
+
348
+ Returns:
349
+ Whether to stop execution
350
+ """
351
+ # Check if any downstream nodes depend on this node
352
+ has_dependents = workflow.graph.out_degree(node_id) > 0
353
+
354
+ # For now, stop if the failed node has dependents
355
+ # Future: implement configurable error handling policies
356
+ return has_dependents