kailash 0.2.2__py3-none-any.whl → 0.3.1__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 +1 -1
- kailash/access_control.py +40 -39
- kailash/api/auth.py +26 -32
- kailash/api/custom_nodes.py +29 -29
- kailash/api/custom_nodes_secure.py +35 -35
- kailash/api/database.py +17 -17
- kailash/api/gateway.py +19 -19
- kailash/api/mcp_integration.py +24 -23
- kailash/api/studio.py +45 -45
- kailash/api/workflow_api.py +8 -8
- kailash/cli/commands.py +5 -8
- kailash/manifest.py +42 -42
- kailash/mcp/__init__.py +1 -1
- kailash/mcp/ai_registry_server.py +20 -20
- kailash/mcp/client.py +9 -11
- kailash/mcp/client_new.py +10 -10
- kailash/mcp/server.py +1 -2
- kailash/mcp/server_enhanced.py +449 -0
- kailash/mcp/servers/ai_registry.py +6 -6
- kailash/mcp/utils/__init__.py +31 -0
- kailash/mcp/utils/cache.py +267 -0
- kailash/mcp/utils/config.py +263 -0
- kailash/mcp/utils/formatters.py +293 -0
- kailash/mcp/utils/metrics.py +418 -0
- kailash/nodes/ai/agents.py +9 -9
- kailash/nodes/ai/ai_providers.py +33 -34
- kailash/nodes/ai/embedding_generator.py +31 -32
- kailash/nodes/ai/intelligent_agent_orchestrator.py +62 -66
- kailash/nodes/ai/iterative_llm_agent.py +48 -48
- kailash/nodes/ai/llm_agent.py +32 -33
- kailash/nodes/ai/models.py +13 -13
- kailash/nodes/ai/self_organizing.py +44 -44
- kailash/nodes/api/__init__.py +5 -0
- kailash/nodes/api/auth.py +11 -11
- kailash/nodes/api/graphql.py +13 -13
- kailash/nodes/api/http.py +19 -19
- kailash/nodes/api/monitoring.py +463 -0
- kailash/nodes/api/rate_limiting.py +9 -13
- kailash/nodes/api/rest.py +29 -29
- kailash/nodes/api/security.py +819 -0
- kailash/nodes/base.py +24 -26
- kailash/nodes/base_async.py +7 -7
- kailash/nodes/base_cycle_aware.py +12 -12
- kailash/nodes/base_with_acl.py +5 -5
- kailash/nodes/code/python.py +56 -55
- kailash/nodes/data/__init__.py +6 -0
- kailash/nodes/data/directory.py +6 -6
- kailash/nodes/data/event_generation.py +297 -0
- kailash/nodes/data/file_discovery.py +598 -0
- kailash/nodes/data/readers.py +8 -8
- kailash/nodes/data/retrieval.py +10 -10
- kailash/nodes/data/sharepoint_graph.py +17 -17
- kailash/nodes/data/sources.py +5 -5
- kailash/nodes/data/sql.py +13 -13
- kailash/nodes/data/streaming.py +25 -25
- kailash/nodes/data/vector_db.py +22 -22
- kailash/nodes/data/writers.py +7 -7
- kailash/nodes/logic/async_operations.py +17 -17
- kailash/nodes/logic/convergence.py +11 -11
- kailash/nodes/logic/loop.py +4 -4
- kailash/nodes/logic/operations.py +11 -11
- kailash/nodes/logic/workflow.py +8 -9
- kailash/nodes/mixins/mcp.py +17 -17
- kailash/nodes/mixins.py +8 -10
- kailash/nodes/transform/chunkers.py +3 -3
- kailash/nodes/transform/formatters.py +7 -7
- kailash/nodes/transform/processors.py +11 -11
- kailash/runtime/access_controlled.py +18 -18
- kailash/runtime/async_local.py +18 -20
- kailash/runtime/docker.py +24 -26
- kailash/runtime/local.py +55 -31
- kailash/runtime/parallel.py +25 -25
- kailash/runtime/parallel_cyclic.py +29 -29
- kailash/runtime/runner.py +6 -6
- kailash/runtime/testing.py +22 -22
- kailash/sdk_exceptions.py +0 -58
- kailash/security.py +14 -26
- kailash/tracking/manager.py +38 -38
- kailash/tracking/metrics_collector.py +15 -14
- kailash/tracking/models.py +53 -53
- kailash/tracking/storage/base.py +7 -17
- kailash/tracking/storage/database.py +22 -23
- kailash/tracking/storage/filesystem.py +38 -40
- kailash/utils/export.py +21 -21
- kailash/utils/templates.py +8 -9
- kailash/visualization/api.py +30 -34
- kailash/visualization/dashboard.py +17 -17
- kailash/visualization/performance.py +32 -19
- kailash/visualization/reports.py +30 -28
- kailash/workflow/builder.py +8 -8
- kailash/workflow/convergence.py +13 -12
- kailash/workflow/cycle_analyzer.py +38 -33
- kailash/workflow/cycle_builder.py +12 -12
- kailash/workflow/cycle_config.py +16 -15
- kailash/workflow/cycle_debugger.py +40 -40
- kailash/workflow/cycle_exceptions.py +29 -29
- kailash/workflow/cycle_profiler.py +21 -21
- kailash/workflow/cycle_state.py +20 -22
- kailash/workflow/cyclic_runner.py +45 -45
- kailash/workflow/graph.py +57 -45
- kailash/workflow/mermaid_visualizer.py +9 -11
- kailash/workflow/migration.py +22 -22
- kailash/workflow/mock_registry.py +6 -6
- kailash/workflow/runner.py +9 -9
- kailash/workflow/safety.py +12 -13
- kailash/workflow/state.py +8 -11
- kailash/workflow/templates.py +19 -19
- kailash/workflow/validation.py +14 -14
- kailash/workflow/visualization.py +32 -24
- kailash-0.3.1.dist-info/METADATA +476 -0
- kailash-0.3.1.dist-info/RECORD +136 -0
- kailash-0.2.2.dist-info/METADATA +0 -121
- kailash-0.2.2.dist-info/RECORD +0 -126
- {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/WHEEL +0 -0
- {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/entry_points.txt +0 -0
- {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/top_level.txt +0 -0
kailash/runtime/docker.py
CHANGED
@@ -21,7 +21,7 @@ import subprocess
|
|
21
21
|
import sys
|
22
22
|
import tempfile
|
23
23
|
from pathlib import Path
|
24
|
-
from typing import Any
|
24
|
+
from typing import Any
|
25
25
|
|
26
26
|
from kailash.nodes.base import Node
|
27
27
|
|
@@ -50,8 +50,8 @@ class DockerNodeWrapper:
|
|
50
50
|
node: Node,
|
51
51
|
node_id: str,
|
52
52
|
base_image: str = "python:3.11-slim",
|
53
|
-
work_dir:
|
54
|
-
sdk_path:
|
53
|
+
work_dir: Path | None = None,
|
54
|
+
sdk_path: Path | None = None,
|
55
55
|
):
|
56
56
|
"""
|
57
57
|
Initialize a Docker node wrapper.
|
@@ -174,7 +174,7 @@ def main():
|
|
174
174
|
logger.info(f"Loaded configuration for {node_data['class']} node")
|
175
175
|
|
176
176
|
# Load runtime inputs if available
|
177
|
-
input_path = Path("/
|
177
|
+
input_path = Path("/data/inputs/json/inputs.json")
|
178
178
|
runtime_inputs = {}
|
179
179
|
if input_path.exists():
|
180
180
|
logger.info(f"Loading inputs from {input_path}")
|
@@ -206,7 +206,7 @@ def main():
|
|
206
206
|
except Exception as e:
|
207
207
|
logger.error(f"Node execution failed: {e}")
|
208
208
|
# Save error information
|
209
|
-
with open("/
|
209
|
+
with open("/data/outputs/json/error.json", 'w') as f:
|
210
210
|
json.dump({
|
211
211
|
"error": str(e),
|
212
212
|
"type": e.__class__.__name__
|
@@ -216,7 +216,7 @@ def main():
|
|
216
216
|
# Save results
|
217
217
|
logger.info("Saving execution results")
|
218
218
|
try:
|
219
|
-
result_path = Path("/
|
219
|
+
result_path = Path("/data/outputs/json/result.json")
|
220
220
|
with open(result_path, 'w') as f:
|
221
221
|
# Handle non-serializable objects with basic conversion
|
222
222
|
try:
|
@@ -335,7 +335,7 @@ ENTRYPOINT ["/app/entrypoint.py"]
|
|
335
335
|
logger.error(error_msg)
|
336
336
|
raise RuntimeError(error_msg)
|
337
337
|
|
338
|
-
def prepare_inputs(self, inputs:
|
338
|
+
def prepare_inputs(self, inputs: dict[str, Any]):
|
339
339
|
"""
|
340
340
|
Prepare inputs for node execution.
|
341
341
|
|
@@ -349,8 +349,8 @@ ENTRYPOINT ["/app/entrypoint.py"]
|
|
349
349
|
def run_container(
|
350
350
|
self,
|
351
351
|
network: str = None,
|
352
|
-
env_vars:
|
353
|
-
resource_limits:
|
352
|
+
env_vars: dict[str, str] = None,
|
353
|
+
resource_limits: dict[str, str] = None,
|
354
354
|
) -> bool:
|
355
355
|
"""
|
356
356
|
Run the node in a Docker container.
|
@@ -416,13 +416,13 @@ ENTRYPOINT ["/app/entrypoint.py"]
|
|
416
416
|
# Check if there's an error file
|
417
417
|
error_file = self.output_dir / "error.json"
|
418
418
|
if error_file.exists():
|
419
|
-
with open(error_file
|
419
|
+
with open(error_file) as f:
|
420
420
|
error_data = json.load(f)
|
421
421
|
error_msg = f"Node execution error: {error_data.get('error', 'Unknown error')}"
|
422
422
|
|
423
423
|
raise NodeExecutionError(error_msg)
|
424
424
|
|
425
|
-
def get_results(self) ->
|
425
|
+
def get_results(self) -> dict[str, Any]:
|
426
426
|
"""
|
427
427
|
Get the results of node execution.
|
428
428
|
|
@@ -431,12 +431,12 @@ ENTRYPOINT ["/app/entrypoint.py"]
|
|
431
431
|
"""
|
432
432
|
result_file = self.output_dir / "result.json"
|
433
433
|
if result_file.exists():
|
434
|
-
with open(result_file
|
434
|
+
with open(result_file) as f:
|
435
435
|
return json.load(f)
|
436
436
|
|
437
437
|
error_file = self.output_dir / "error.json"
|
438
438
|
if error_file.exists():
|
439
|
-
with open(error_file
|
439
|
+
with open(error_file) as f:
|
440
440
|
error_data = json.load(f)
|
441
441
|
raise NodeExecutionError(
|
442
442
|
f"Node {self.node_id} execution failed: {error_data.get('error', 'Unknown error')}"
|
@@ -464,10 +464,10 @@ class DockerRuntime:
|
|
464
464
|
self,
|
465
465
|
base_image: str = "python:3.11-slim",
|
466
466
|
network_name: str = "kailash-network",
|
467
|
-
work_dir:
|
468
|
-
sdk_path:
|
469
|
-
resource_limits:
|
470
|
-
task_manager:
|
467
|
+
work_dir: str | None = None,
|
468
|
+
sdk_path: str | None = None,
|
469
|
+
resource_limits: dict[str, str] | None = None,
|
470
|
+
task_manager: TaskManager | None = None,
|
471
471
|
):
|
472
472
|
"""
|
473
473
|
Initialize the Docker runtime.
|
@@ -516,14 +516,14 @@ class DockerRuntime:
|
|
516
516
|
# Track node wrappers
|
517
517
|
self.node_wrappers = {}
|
518
518
|
|
519
|
-
def _create_task_run(self, workflow: Workflow) ->
|
519
|
+
def _create_task_run(self, workflow: Workflow) -> str | None:
|
520
520
|
"""Create a task run if task manager is available."""
|
521
521
|
if self.task_manager:
|
522
522
|
return self.task_manager.create_run(workflow.name)
|
523
523
|
return None
|
524
524
|
|
525
525
|
def _update_task_status(
|
526
|
-
self, run_id:
|
526
|
+
self, run_id: str | None, node_id: str, status: str, output: Any = None
|
527
527
|
):
|
528
528
|
"""Update task status if task manager is available."""
|
529
529
|
if self.task_manager and run_id:
|
@@ -534,9 +534,7 @@ class DockerRuntime:
|
|
534
534
|
)
|
535
535
|
self.task_manager.update_run_status(run_id, "failed", error_msg)
|
536
536
|
|
537
|
-
def _complete_task_run(
|
538
|
-
self, run_id: Optional[str], status: str, result: Any = None
|
539
|
-
):
|
537
|
+
def _complete_task_run(self, run_id: str | None, status: str, result: Any = None):
|
540
538
|
"""Complete task run if task manager is available."""
|
541
539
|
if self.task_manager and run_id:
|
542
540
|
if status == "completed":
|
@@ -567,9 +565,9 @@ class DockerRuntime:
|
|
567
565
|
def execute(
|
568
566
|
self,
|
569
567
|
workflow: Workflow,
|
570
|
-
inputs:
|
571
|
-
node_resource_limits:
|
572
|
-
) ->
|
568
|
+
inputs: dict[str, dict[str, Any]] = None,
|
569
|
+
node_resource_limits: dict[str, dict[str, str]] = None,
|
570
|
+
) -> tuple[dict[str, dict[str, Any]], str]:
|
573
571
|
"""
|
574
572
|
Execute a workflow using Docker containers.
|
575
573
|
|
@@ -590,7 +588,7 @@ class DockerRuntime:
|
|
590
588
|
|
591
589
|
try:
|
592
590
|
# Validate workflow
|
593
|
-
workflow.validate()
|
591
|
+
workflow.validate(runtime_parameters=inputs)
|
594
592
|
|
595
593
|
# Get execution order
|
596
594
|
execution_order = workflow.get_execution_order()
|
kailash/runtime/local.py
CHANGED
@@ -40,8 +40,8 @@ Examples:
|
|
40
40
|
"""
|
41
41
|
|
42
42
|
import logging
|
43
|
-
from datetime import
|
44
|
-
from typing import Any
|
43
|
+
from datetime import UTC, datetime
|
44
|
+
from typing import Any
|
45
45
|
|
46
46
|
import networkx as nx
|
47
47
|
|
@@ -90,9 +90,9 @@ class LocalRuntime:
|
|
90
90
|
def execute(
|
91
91
|
self,
|
92
92
|
workflow: Workflow,
|
93
|
-
task_manager:
|
94
|
-
parameters:
|
95
|
-
) ->
|
93
|
+
task_manager: TaskManager | None = None,
|
94
|
+
parameters: dict[str, dict[str, Any]] | None = None,
|
95
|
+
) -> tuple[dict[str, Any], str | None]:
|
96
96
|
"""Execute a workflow locally.
|
97
97
|
|
98
98
|
Args:
|
@@ -113,8 +113,8 @@ class LocalRuntime:
|
|
113
113
|
run_id = None
|
114
114
|
|
115
115
|
try:
|
116
|
-
# Validate workflow
|
117
|
-
workflow.validate()
|
116
|
+
# Validate workflow with runtime parameters (Session 061)
|
117
|
+
workflow.validate(runtime_parameters=parameters)
|
118
118
|
|
119
119
|
# Initialize tracking
|
120
120
|
if task_manager:
|
@@ -197,10 +197,10 @@ class LocalRuntime:
|
|
197
197
|
def _execute_workflow(
|
198
198
|
self,
|
199
199
|
workflow: Workflow,
|
200
|
-
task_manager:
|
201
|
-
run_id:
|
202
|
-
parameters:
|
203
|
-
) ->
|
200
|
+
task_manager: TaskManager | None,
|
201
|
+
run_id: str | None,
|
202
|
+
parameters: dict[str, dict[str, Any]],
|
203
|
+
) -> dict[str, Any]:
|
204
204
|
"""Execute the workflow nodes in topological order.
|
205
205
|
|
206
206
|
Args:
|
@@ -273,7 +273,7 @@ class LocalRuntime:
|
|
273
273
|
run_id=run_id,
|
274
274
|
node_id=node_id,
|
275
275
|
node_type=node_instance.__class__.__name__,
|
276
|
-
started_at=datetime.now(
|
276
|
+
started_at=datetime.now(UTC),
|
277
277
|
metadata=node_metadata,
|
278
278
|
)
|
279
279
|
# Start the task
|
@@ -296,6 +296,10 @@ class LocalRuntime:
|
|
296
296
|
parameters=parameters.get(node_id, {}),
|
297
297
|
)
|
298
298
|
|
299
|
+
# Update node config with parameters (Session 061: direct config update)
|
300
|
+
{**node_instance.config, **parameters.get(node_id, {})}
|
301
|
+
node_instance.config.update(parameters.get(node_id, {}))
|
302
|
+
|
299
303
|
if self.debug:
|
300
304
|
self.logger.debug(f"Node {node_id} inputs: {inputs}")
|
301
305
|
|
@@ -325,7 +329,7 @@ class LocalRuntime:
|
|
325
329
|
task.task_id,
|
326
330
|
TaskStatus.COMPLETED,
|
327
331
|
result=outputs,
|
328
|
-
ended_at=datetime.now(
|
332
|
+
ended_at=datetime.now(UTC),
|
329
333
|
metadata={"execution_time": performance_metrics.duration},
|
330
334
|
)
|
331
335
|
|
@@ -346,7 +350,7 @@ class LocalRuntime:
|
|
346
350
|
task.task_id,
|
347
351
|
TaskStatus.FAILED,
|
348
352
|
error=str(e),
|
349
|
-
ended_at=datetime.now(
|
353
|
+
ended_at=datetime.now(UTC),
|
350
354
|
)
|
351
355
|
|
352
356
|
# Determine if we should continue
|
@@ -371,9 +375,9 @@ class LocalRuntime:
|
|
371
375
|
workflow: Workflow,
|
372
376
|
node_id: str,
|
373
377
|
node_instance: Node,
|
374
|
-
node_outputs:
|
375
|
-
parameters:
|
376
|
-
) ->
|
378
|
+
node_outputs: dict[str, dict[str, Any]],
|
379
|
+
parameters: dict[str, Any],
|
380
|
+
) -> dict[str, Any]:
|
377
381
|
"""Prepare inputs for a node execution.
|
378
382
|
|
379
383
|
Args:
|
@@ -391,21 +395,36 @@ class LocalRuntime:
|
|
391
395
|
"""
|
392
396
|
inputs = {}
|
393
397
|
|
394
|
-
#
|
395
|
-
inputs
|
398
|
+
# NOTE: Node configuration is handled separately in configure() call
|
399
|
+
# Only add runtime inputs and data from connected nodes here
|
400
|
+
|
401
|
+
# Add runtime parameters (those not used for node configuration)
|
402
|
+
# Map specific runtime parameters for known node types
|
403
|
+
if "consumer_timeout_ms" in parameters:
|
404
|
+
inputs["timeout_ms"] = parameters["consumer_timeout_ms"]
|
405
|
+
|
406
|
+
# Add other potential runtime parameters that are not configuration
|
407
|
+
runtime_param_names = {"max_messages", "timeout_ms", "limit", "offset"}
|
408
|
+
for param_name, param_value in parameters.items():
|
409
|
+
if param_name in runtime_param_names:
|
410
|
+
inputs[param_name] = param_value
|
396
411
|
|
397
412
|
# Add connected inputs from other nodes
|
398
413
|
for edge in workflow.graph.in_edges(node_id, data=True):
|
399
414
|
source_node_id = edge[0]
|
400
415
|
mapping = edge[2].get("mapping", {})
|
401
416
|
|
402
|
-
|
403
|
-
|
404
|
-
|
417
|
+
if self.debug:
|
418
|
+
self.logger.debug(f"Processing edge {source_node_id} -> {node_id}")
|
419
|
+
self.logger.debug(f" Edge data: {edge[2]}")
|
420
|
+
self.logger.debug(f" Mapping: {mapping}")
|
405
421
|
|
406
422
|
if source_node_id in node_outputs:
|
407
423
|
source_outputs = node_outputs[source_node_id]
|
408
|
-
|
424
|
+
if self.debug:
|
425
|
+
self.logger.debug(
|
426
|
+
f" Source outputs: {list(source_outputs.keys())}"
|
427
|
+
)
|
409
428
|
|
410
429
|
# Check if the source node failed
|
411
430
|
if isinstance(source_outputs, dict) and source_outputs.get("failed"):
|
@@ -416,19 +435,24 @@ class LocalRuntime:
|
|
416
435
|
for source_key, target_key in mapping.items():
|
417
436
|
if source_key in source_outputs:
|
418
437
|
inputs[target_key] = source_outputs[source_key]
|
419
|
-
|
420
|
-
|
421
|
-
|
438
|
+
if self.debug:
|
439
|
+
self.logger.debug(
|
440
|
+
f" MAPPED: {source_key} -> {target_key} (type: {type(source_outputs[source_key])})"
|
441
|
+
)
|
422
442
|
else:
|
423
|
-
|
424
|
-
|
425
|
-
|
443
|
+
if self.debug:
|
444
|
+
self.logger.debug(
|
445
|
+
f" MISSING: {source_key} not in {list(source_outputs.keys())}"
|
446
|
+
)
|
426
447
|
self.logger.warning(
|
427
448
|
f"Source output '{source_key}' not found in node '{source_node_id}'. "
|
428
449
|
f"Available outputs: {list(source_outputs.keys())}"
|
429
450
|
)
|
430
451
|
else:
|
431
|
-
|
452
|
+
if self.debug:
|
453
|
+
self.logger.debug(
|
454
|
+
f" No outputs found for source node {source_node_id}"
|
455
|
+
)
|
432
456
|
|
433
457
|
# Apply parameter overrides
|
434
458
|
inputs.update(parameters)
|
@@ -452,7 +476,7 @@ class LocalRuntime:
|
|
452
476
|
# Future: implement configurable error handling policies
|
453
477
|
return has_dependents
|
454
478
|
|
455
|
-
def validate_workflow(self, workflow: Workflow) ->
|
479
|
+
def validate_workflow(self, workflow: Workflow) -> list[str]:
|
456
480
|
"""Validate a workflow before execution.
|
457
481
|
|
458
482
|
Args:
|
kailash/runtime/parallel.py
CHANGED
@@ -8,8 +8,8 @@ import asyncio
|
|
8
8
|
import logging
|
9
9
|
import time
|
10
10
|
from collections import deque
|
11
|
-
from datetime import
|
12
|
-
from typing import Any
|
11
|
+
from datetime import UTC, datetime
|
12
|
+
from typing import Any
|
13
13
|
|
14
14
|
import networkx as nx
|
15
15
|
|
@@ -66,9 +66,9 @@ class ParallelRuntime:
|
|
66
66
|
async def execute(
|
67
67
|
self,
|
68
68
|
workflow: Workflow,
|
69
|
-
task_manager:
|
70
|
-
parameters:
|
71
|
-
) ->
|
69
|
+
task_manager: TaskManager | None = None,
|
70
|
+
parameters: dict[str, dict[str, Any]] | None = None,
|
71
|
+
) -> tuple[dict[str, Any], str | None]:
|
72
72
|
"""Execute a workflow with parallel node execution.
|
73
73
|
|
74
74
|
Args:
|
@@ -91,7 +91,7 @@ class ParallelRuntime:
|
|
91
91
|
|
92
92
|
try:
|
93
93
|
# Validate workflow
|
94
|
-
workflow.validate()
|
94
|
+
workflow.validate(runtime_parameters=parameters)
|
95
95
|
|
96
96
|
# Initialize semaphore for concurrent execution control
|
97
97
|
self.semaphore = asyncio.Semaphore(self.max_workers)
|
@@ -159,10 +159,10 @@ class ParallelRuntime:
|
|
159
159
|
async def _execute_workflow_parallel(
|
160
160
|
self,
|
161
161
|
workflow: Workflow,
|
162
|
-
task_manager:
|
163
|
-
run_id:
|
164
|
-
parameters:
|
165
|
-
) ->
|
162
|
+
task_manager: TaskManager | None,
|
163
|
+
run_id: str | None,
|
164
|
+
parameters: dict[str, dict[str, Any]],
|
165
|
+
) -> dict[str, Any]:
|
166
166
|
"""Execute the workflow nodes in parallel where possible.
|
167
167
|
|
168
168
|
This method uses a dynamic scheduling approach to run independent nodes
|
@@ -323,11 +323,11 @@ class ParallelRuntime:
|
|
323
323
|
self,
|
324
324
|
workflow: Workflow,
|
325
325
|
node_id: str,
|
326
|
-
node_outputs:
|
327
|
-
parameters:
|
328
|
-
task_manager:
|
329
|
-
run_id:
|
330
|
-
) ->
|
326
|
+
node_outputs: dict[str, dict[str, Any]],
|
327
|
+
parameters: dict[str, Any],
|
328
|
+
task_manager: TaskManager | None,
|
329
|
+
run_id: str | None,
|
330
|
+
) -> tuple[dict[str, Any], bool]:
|
331
331
|
"""Execute a single node asynchronously.
|
332
332
|
|
333
333
|
Args:
|
@@ -359,7 +359,7 @@ class ParallelRuntime:
|
|
359
359
|
run_id=run_id,
|
360
360
|
node_id=node_id,
|
361
361
|
node_type=node_instance.__class__.__name__,
|
362
|
-
started_at=datetime.now(
|
362
|
+
started_at=datetime.now(UTC),
|
363
363
|
)
|
364
364
|
except Exception as e:
|
365
365
|
self.logger.warning(f"Failed to create task for node '{node_id}': {e}")
|
@@ -398,7 +398,7 @@ class ParallelRuntime:
|
|
398
398
|
async def execute_with_metrics():
|
399
399
|
with collector.collect(node_id=node_id) as context:
|
400
400
|
result = await loop.run_in_executor(
|
401
|
-
None, lambda: node_instance.
|
401
|
+
None, lambda: node_instance.run(**inputs)
|
402
402
|
)
|
403
403
|
return result, context.result()
|
404
404
|
|
@@ -409,7 +409,7 @@ class ParallelRuntime:
|
|
409
409
|
task.update_status(
|
410
410
|
TaskStatus.COMPLETED,
|
411
411
|
result=outputs,
|
412
|
-
ended_at=datetime.now(
|
412
|
+
ended_at=datetime.now(UTC),
|
413
413
|
metadata={"execution_time": performance_metrics.duration},
|
414
414
|
)
|
415
415
|
|
@@ -431,7 +431,7 @@ class ParallelRuntime:
|
|
431
431
|
# Update task status
|
432
432
|
if task:
|
433
433
|
task.update_status(
|
434
|
-
TaskStatus.FAILED, error=str(e), ended_at=datetime.now(
|
434
|
+
TaskStatus.FAILED, error=str(e), ended_at=datetime.now(UTC)
|
435
435
|
)
|
436
436
|
|
437
437
|
# Return error result
|
@@ -448,9 +448,9 @@ class ParallelRuntime:
|
|
448
448
|
workflow: Workflow,
|
449
449
|
node_id: str,
|
450
450
|
node_instance: Any,
|
451
|
-
node_outputs:
|
452
|
-
parameters:
|
453
|
-
) ->
|
451
|
+
node_outputs: dict[str, dict[str, Any]],
|
452
|
+
parameters: dict[str, Any],
|
453
|
+
) -> dict[str, Any]:
|
454
454
|
"""Prepare inputs for a node execution.
|
455
455
|
|
456
456
|
Args:
|
@@ -520,9 +520,9 @@ class ParallelRuntime:
|
|
520
520
|
self,
|
521
521
|
workflow: Workflow,
|
522
522
|
failed_node: str,
|
523
|
-
failed_nodes:
|
524
|
-
pending_nodes:
|
525
|
-
ready_nodes:
|
523
|
+
failed_nodes: set[str],
|
524
|
+
pending_nodes: set[str],
|
525
|
+
ready_nodes: deque[str],
|
526
526
|
) -> None:
|
527
527
|
"""Mark all dependent nodes as failed.
|
528
528
|
|
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
import logging
|
4
4
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
5
|
-
from datetime import
|
6
|
-
from typing import Any
|
5
|
+
from datetime import UTC, datetime
|
6
|
+
from typing import Any
|
7
7
|
|
8
8
|
import networkx as nx
|
9
9
|
|
@@ -56,10 +56,10 @@ class ParallelCyclicRuntime:
|
|
56
56
|
def execute(
|
57
57
|
self,
|
58
58
|
workflow: Workflow,
|
59
|
-
task_manager:
|
60
|
-
parameters:
|
61
|
-
parallel_nodes:
|
62
|
-
) ->
|
59
|
+
task_manager: TaskManager | None = None,
|
60
|
+
parameters: dict[str, dict[str, Any]] | None = None,
|
61
|
+
parallel_nodes: set[str] | None = None,
|
62
|
+
) -> tuple[dict[str, Any], str | None]:
|
63
63
|
"""Execute a workflow with parallel and cyclic support.
|
64
64
|
|
65
65
|
Args:
|
@@ -80,7 +80,7 @@ class ParallelCyclicRuntime:
|
|
80
80
|
|
81
81
|
try:
|
82
82
|
# Validate workflow
|
83
|
-
workflow.validate()
|
83
|
+
workflow.validate(runtime_parameters=parameters)
|
84
84
|
|
85
85
|
# Check for cycles first
|
86
86
|
if self.enable_cycles and workflow.has_cycles():
|
@@ -108,9 +108,9 @@ class ParallelCyclicRuntime:
|
|
108
108
|
def _execute_cyclic_workflow(
|
109
109
|
self,
|
110
110
|
workflow: Workflow,
|
111
|
-
task_manager:
|
112
|
-
parameters:
|
113
|
-
) ->
|
111
|
+
task_manager: TaskManager | None,
|
112
|
+
parameters: dict[str, dict[str, Any]] | None,
|
113
|
+
) -> tuple[dict[str, Any], str]:
|
114
114
|
"""Execute a cyclic workflow with potential parallel optimizations.
|
115
115
|
|
116
116
|
Args:
|
@@ -141,10 +141,10 @@ class ParallelCyclicRuntime:
|
|
141
141
|
def _execute_parallel_dag(
|
142
142
|
self,
|
143
143
|
workflow: Workflow,
|
144
|
-
task_manager:
|
145
|
-
parameters:
|
146
|
-
parallel_nodes:
|
147
|
-
) ->
|
144
|
+
task_manager: TaskManager | None,
|
145
|
+
parameters: dict[str, dict[str, Any]] | None,
|
146
|
+
parallel_nodes: set[str] | None,
|
147
|
+
) -> tuple[dict[str, Any], str | None]:
|
148
148
|
"""Execute a DAG workflow with parallel node execution.
|
149
149
|
|
150
150
|
Args:
|
@@ -252,8 +252,8 @@ class ParallelCyclicRuntime:
|
|
252
252
|
raise
|
253
253
|
|
254
254
|
def _analyze_parallel_groups(
|
255
|
-
self, workflow: Workflow, parallel_nodes:
|
256
|
-
) ->
|
255
|
+
self, workflow: Workflow, parallel_nodes: set[str] | None
|
256
|
+
) -> list[list[str]]:
|
257
257
|
"""Analyze workflow to identify groups of nodes that can be executed in parallel.
|
258
258
|
|
259
259
|
Args:
|
@@ -320,11 +320,11 @@ class ParallelCyclicRuntime:
|
|
320
320
|
self,
|
321
321
|
workflow: Workflow,
|
322
322
|
node_id: str,
|
323
|
-
previous_results:
|
324
|
-
parameters:
|
325
|
-
task_manager:
|
326
|
-
run_id:
|
327
|
-
) ->
|
323
|
+
previous_results: dict[str, Any],
|
324
|
+
parameters: dict[str, dict[str, Any]] | None,
|
325
|
+
task_manager: TaskManager | None,
|
326
|
+
run_id: str | None,
|
327
|
+
) -> dict[str, Any]:
|
328
328
|
"""Execute a single node in isolation.
|
329
329
|
|
330
330
|
Args:
|
@@ -356,7 +356,7 @@ class ParallelCyclicRuntime:
|
|
356
356
|
run_id=run_id,
|
357
357
|
node_id=node_id,
|
358
358
|
node_type=node_instance.__class__.__name__,
|
359
|
-
started_at=datetime.now(
|
359
|
+
started_at=datetime.now(UTC),
|
360
360
|
metadata={},
|
361
361
|
)
|
362
362
|
if task:
|
@@ -380,7 +380,7 @@ class ParallelCyclicRuntime:
|
|
380
380
|
# Execute node with metrics collection
|
381
381
|
collector = MetricsCollector()
|
382
382
|
with collector.collect(node_id=node_id) as metrics_context:
|
383
|
-
outputs = node_instance.
|
383
|
+
outputs = node_instance.run(**inputs)
|
384
384
|
|
385
385
|
# Get performance metrics
|
386
386
|
performance_metrics = metrics_context.result()
|
@@ -397,7 +397,7 @@ class ParallelCyclicRuntime:
|
|
397
397
|
task.task_id,
|
398
398
|
TaskStatus.COMPLETED,
|
399
399
|
result=outputs,
|
400
|
-
ended_at=datetime.now(
|
400
|
+
ended_at=datetime.now(UTC),
|
401
401
|
metadata={"execution_time": performance_metrics.duration},
|
402
402
|
)
|
403
403
|
task_manager.update_task_metrics(task.task_id, task_metrics)
|
@@ -415,7 +415,7 @@ class ParallelCyclicRuntime:
|
|
415
415
|
task.task_id,
|
416
416
|
TaskStatus.FAILED,
|
417
417
|
error=str(e),
|
418
|
-
ended_at=datetime.now(
|
418
|
+
ended_at=datetime.now(UTC),
|
419
419
|
)
|
420
420
|
|
421
421
|
self.logger.error(f"Node {node_id} failed: {e}", exc_info=self.debug)
|
@@ -428,9 +428,9 @@ class ParallelCyclicRuntime:
|
|
428
428
|
workflow: Workflow,
|
429
429
|
node_id: str,
|
430
430
|
node_instance: Node,
|
431
|
-
previous_results:
|
432
|
-
parameters:
|
433
|
-
) ->
|
431
|
+
previous_results: dict[str, Any],
|
432
|
+
parameters: dict[str, Any],
|
433
|
+
) -> dict[str, Any]:
|
434
434
|
"""Prepare inputs for a node execution in parallel context.
|
435
435
|
|
436
436
|
Args:
|
@@ -509,7 +509,7 @@ class ParallelCyclicRuntime:
|
|
509
509
|
return False
|
510
510
|
|
511
511
|
def _should_stop_on_group_error(
|
512
|
-
self, workflow: Workflow, failed_node: str, node_group:
|
512
|
+
self, workflow: Workflow, failed_node: str, node_group: list[str]
|
513
513
|
) -> bool:
|
514
514
|
"""Determine if execution should stop when a node in a parallel group fails.
|
515
515
|
|
kailash/runtime/runner.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Main runner for workflow execution."""
|
2
2
|
|
3
3
|
import logging
|
4
|
-
from typing import Any
|
4
|
+
from typing import Any
|
5
5
|
|
6
6
|
from kailash.runtime.local import LocalRuntime
|
7
7
|
from kailash.tracking import TaskManager
|
@@ -11,7 +11,7 @@ from kailash.workflow import Workflow
|
|
11
11
|
class WorkflowRunner:
|
12
12
|
"""High-level interface for running workflows."""
|
13
13
|
|
14
|
-
def __init__(self, debug: bool = False, task_manager:
|
14
|
+
def __init__(self, debug: bool = False, task_manager: TaskManager | None = None):
|
15
15
|
"""Initialize the workflow runner.
|
16
16
|
|
17
17
|
Args:
|
@@ -36,9 +36,9 @@ class WorkflowRunner:
|
|
36
36
|
def run(
|
37
37
|
self,
|
38
38
|
workflow: Workflow,
|
39
|
-
parameters:
|
39
|
+
parameters: dict[str, dict[str, Any]] | None = None,
|
40
40
|
runtime_type: str = "local",
|
41
|
-
) ->
|
41
|
+
) -> tuple[dict[str, Any], str]:
|
42
42
|
"""Run a workflow.
|
43
43
|
|
44
44
|
Args:
|
@@ -82,7 +82,7 @@ class WorkflowRunner:
|
|
82
82
|
runtime = LocalRuntime(debug=self.debug)
|
83
83
|
return runtime.validate_workflow(workflow)
|
84
84
|
|
85
|
-
def get_run_status(self, run_id: str) ->
|
85
|
+
def get_run_status(self, run_id: str) -> dict[str, Any]:
|
86
86
|
"""Get status of a workflow run.
|
87
87
|
|
88
88
|
Args:
|
@@ -94,7 +94,7 @@ class WorkflowRunner:
|
|
94
94
|
return self.task_manager.get_run_status(run_id)
|
95
95
|
|
96
96
|
def get_run_history(
|
97
|
-
self, workflow_name:
|
97
|
+
self, workflow_name: str | None = None, limit: int = 10
|
98
98
|
) -> list:
|
99
99
|
"""Get run history.
|
100
100
|
|