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.
- kailash/__init__.py +31 -0
- kailash/__main__.py +11 -0
- kailash/cli/__init__.py +5 -0
- kailash/cli/commands.py +563 -0
- kailash/manifest.py +778 -0
- kailash/nodes/__init__.py +23 -0
- kailash/nodes/ai/__init__.py +26 -0
- kailash/nodes/ai/agents.py +417 -0
- kailash/nodes/ai/models.py +488 -0
- kailash/nodes/api/__init__.py +52 -0
- kailash/nodes/api/auth.py +567 -0
- kailash/nodes/api/graphql.py +480 -0
- kailash/nodes/api/http.py +598 -0
- kailash/nodes/api/rate_limiting.py +572 -0
- kailash/nodes/api/rest.py +665 -0
- kailash/nodes/base.py +1032 -0
- kailash/nodes/base_async.py +128 -0
- kailash/nodes/code/__init__.py +32 -0
- kailash/nodes/code/python.py +1021 -0
- kailash/nodes/data/__init__.py +125 -0
- kailash/nodes/data/readers.py +496 -0
- kailash/nodes/data/sharepoint_graph.py +623 -0
- kailash/nodes/data/sql.py +380 -0
- kailash/nodes/data/streaming.py +1168 -0
- kailash/nodes/data/vector_db.py +964 -0
- kailash/nodes/data/writers.py +529 -0
- kailash/nodes/logic/__init__.py +6 -0
- kailash/nodes/logic/async_operations.py +702 -0
- kailash/nodes/logic/operations.py +551 -0
- kailash/nodes/transform/__init__.py +5 -0
- kailash/nodes/transform/processors.py +379 -0
- kailash/runtime/__init__.py +6 -0
- kailash/runtime/async_local.py +356 -0
- kailash/runtime/docker.py +697 -0
- kailash/runtime/local.py +434 -0
- kailash/runtime/parallel.py +557 -0
- kailash/runtime/runner.py +110 -0
- kailash/runtime/testing.py +347 -0
- kailash/sdk_exceptions.py +307 -0
- kailash/tracking/__init__.py +7 -0
- kailash/tracking/manager.py +885 -0
- kailash/tracking/metrics_collector.py +342 -0
- kailash/tracking/models.py +535 -0
- kailash/tracking/storage/__init__.py +0 -0
- kailash/tracking/storage/base.py +113 -0
- kailash/tracking/storage/database.py +619 -0
- kailash/tracking/storage/filesystem.py +543 -0
- kailash/utils/__init__.py +0 -0
- kailash/utils/export.py +924 -0
- kailash/utils/templates.py +680 -0
- kailash/visualization/__init__.py +62 -0
- kailash/visualization/api.py +732 -0
- kailash/visualization/dashboard.py +951 -0
- kailash/visualization/performance.py +808 -0
- kailash/visualization/reports.py +1471 -0
- kailash/workflow/__init__.py +15 -0
- kailash/workflow/builder.py +245 -0
- kailash/workflow/graph.py +827 -0
- kailash/workflow/mermaid_visualizer.py +628 -0
- kailash/workflow/mock_registry.py +63 -0
- kailash/workflow/runner.py +302 -0
- kailash/workflow/state.py +238 -0
- kailash/workflow/visualization.py +588 -0
- kailash-0.1.0.dist-info/METADATA +710 -0
- kailash-0.1.0.dist-info/RECORD +69 -0
- kailash-0.1.0.dist-info/WHEEL +5 -0
- kailash-0.1.0.dist-info/entry_points.txt +2 -0
- kailash-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|