kailash 0.3.1__py3-none-any.whl → 0.4.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 +33 -1
- kailash/access_control/__init__.py +129 -0
- kailash/access_control/managers.py +461 -0
- kailash/access_control/rule_evaluators.py +467 -0
- kailash/access_control_abac.py +825 -0
- kailash/config/__init__.py +27 -0
- kailash/config/database_config.py +359 -0
- kailash/database/__init__.py +28 -0
- kailash/database/execution_pipeline.py +499 -0
- kailash/middleware/__init__.py +306 -0
- kailash/middleware/auth/__init__.py +33 -0
- kailash/middleware/auth/access_control.py +436 -0
- kailash/middleware/auth/auth_manager.py +422 -0
- kailash/middleware/auth/jwt_auth.py +477 -0
- kailash/middleware/auth/kailash_jwt_auth.py +616 -0
- kailash/middleware/communication/__init__.py +37 -0
- kailash/middleware/communication/ai_chat.py +989 -0
- kailash/middleware/communication/api_gateway.py +802 -0
- kailash/middleware/communication/events.py +470 -0
- kailash/middleware/communication/realtime.py +710 -0
- kailash/middleware/core/__init__.py +21 -0
- kailash/middleware/core/agent_ui.py +890 -0
- kailash/middleware/core/schema.py +643 -0
- kailash/middleware/core/workflows.py +396 -0
- kailash/middleware/database/__init__.py +63 -0
- kailash/middleware/database/base.py +113 -0
- kailash/middleware/database/base_models.py +525 -0
- kailash/middleware/database/enums.py +106 -0
- kailash/middleware/database/migrations.py +12 -0
- kailash/{api/database.py → middleware/database/models.py} +183 -291
- kailash/middleware/database/repositories.py +685 -0
- kailash/middleware/database/session_manager.py +19 -0
- kailash/middleware/mcp/__init__.py +38 -0
- kailash/middleware/mcp/client_integration.py +585 -0
- kailash/middleware/mcp/enhanced_server.py +576 -0
- kailash/nodes/__init__.py +25 -3
- kailash/nodes/admin/__init__.py +35 -0
- kailash/nodes/admin/audit_log.py +794 -0
- kailash/nodes/admin/permission_check.py +864 -0
- kailash/nodes/admin/role_management.py +823 -0
- kailash/nodes/admin/security_event.py +1519 -0
- kailash/nodes/admin/user_management.py +944 -0
- kailash/nodes/ai/a2a.py +24 -7
- kailash/nodes/ai/ai_providers.py +1 -0
- kailash/nodes/ai/embedding_generator.py +11 -11
- kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
- kailash/nodes/ai/llm_agent.py +407 -2
- kailash/nodes/ai/self_organizing.py +85 -10
- kailash/nodes/api/auth.py +287 -6
- kailash/nodes/api/rest.py +151 -0
- kailash/nodes/auth/__init__.py +17 -0
- kailash/nodes/auth/directory_integration.py +1228 -0
- kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
- kailash/nodes/auth/mfa.py +2338 -0
- kailash/nodes/auth/risk_assessment.py +872 -0
- kailash/nodes/auth/session_management.py +1093 -0
- kailash/nodes/auth/sso.py +1040 -0
- kailash/nodes/base.py +344 -13
- kailash/nodes/base_cycle_aware.py +4 -2
- kailash/nodes/base_with_acl.py +1 -1
- kailash/nodes/code/python.py +293 -12
- kailash/nodes/compliance/__init__.py +9 -0
- kailash/nodes/compliance/data_retention.py +1888 -0
- kailash/nodes/compliance/gdpr.py +2004 -0
- kailash/nodes/data/__init__.py +22 -2
- kailash/nodes/data/async_connection.py +469 -0
- kailash/nodes/data/async_sql.py +757 -0
- kailash/nodes/data/async_vector.py +598 -0
- kailash/nodes/data/readers.py +767 -0
- kailash/nodes/data/retrieval.py +360 -1
- kailash/nodes/data/sharepoint_graph.py +397 -21
- kailash/nodes/data/sql.py +94 -5
- kailash/nodes/data/streaming.py +68 -8
- kailash/nodes/data/vector_db.py +54 -4
- kailash/nodes/enterprise/__init__.py +13 -0
- kailash/nodes/enterprise/batch_processor.py +741 -0
- kailash/nodes/enterprise/data_lineage.py +497 -0
- kailash/nodes/logic/convergence.py +31 -9
- kailash/nodes/logic/operations.py +14 -3
- kailash/nodes/mixins/__init__.py +8 -0
- kailash/nodes/mixins/event_emitter.py +201 -0
- kailash/nodes/mixins/mcp.py +9 -4
- kailash/nodes/mixins/security.py +165 -0
- kailash/nodes/monitoring/__init__.py +7 -0
- kailash/nodes/monitoring/performance_benchmark.py +2497 -0
- kailash/nodes/rag/__init__.py +284 -0
- kailash/nodes/rag/advanced.py +1615 -0
- kailash/nodes/rag/agentic.py +773 -0
- kailash/nodes/rag/conversational.py +999 -0
- kailash/nodes/rag/evaluation.py +875 -0
- kailash/nodes/rag/federated.py +1188 -0
- kailash/nodes/rag/graph.py +721 -0
- kailash/nodes/rag/multimodal.py +671 -0
- kailash/nodes/rag/optimized.py +933 -0
- kailash/nodes/rag/privacy.py +1059 -0
- kailash/nodes/rag/query_processing.py +1335 -0
- kailash/nodes/rag/realtime.py +764 -0
- kailash/nodes/rag/registry.py +547 -0
- kailash/nodes/rag/router.py +837 -0
- kailash/nodes/rag/similarity.py +1854 -0
- kailash/nodes/rag/strategies.py +566 -0
- kailash/nodes/rag/workflows.py +575 -0
- kailash/nodes/security/__init__.py +19 -0
- kailash/nodes/security/abac_evaluator.py +1411 -0
- kailash/nodes/security/audit_log.py +91 -0
- kailash/nodes/security/behavior_analysis.py +1893 -0
- kailash/nodes/security/credential_manager.py +401 -0
- kailash/nodes/security/rotating_credentials.py +760 -0
- kailash/nodes/security/security_event.py +132 -0
- kailash/nodes/security/threat_detection.py +1103 -0
- kailash/nodes/testing/__init__.py +9 -0
- kailash/nodes/testing/credential_testing.py +499 -0
- kailash/nodes/transform/__init__.py +10 -2
- kailash/nodes/transform/chunkers.py +592 -1
- kailash/nodes/transform/processors.py +484 -14
- kailash/nodes/validation.py +321 -0
- kailash/runtime/access_controlled.py +1 -1
- kailash/runtime/async_local.py +41 -7
- kailash/runtime/docker.py +1 -1
- kailash/runtime/local.py +474 -55
- kailash/runtime/parallel.py +1 -1
- kailash/runtime/parallel_cyclic.py +1 -1
- kailash/runtime/testing.py +210 -2
- kailash/utils/migrations/__init__.py +25 -0
- kailash/utils/migrations/generator.py +433 -0
- kailash/utils/migrations/models.py +231 -0
- kailash/utils/migrations/runner.py +489 -0
- kailash/utils/secure_logging.py +342 -0
- kailash/workflow/__init__.py +16 -0
- kailash/workflow/cyclic_runner.py +3 -4
- kailash/workflow/graph.py +70 -2
- kailash/workflow/resilience.py +249 -0
- kailash/workflow/templates.py +726 -0
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/METADATA +253 -20
- kailash-0.4.0.dist-info/RECORD +223 -0
- kailash/api/__init__.py +0 -17
- kailash/api/__main__.py +0 -6
- kailash/api/studio_secure.py +0 -893
- kailash/mcp/__main__.py +0 -13
- kailash/mcp/server_new.py +0 -336
- kailash/mcp/servers/__init__.py +0 -12
- kailash-0.3.1.dist-info/RECORD +0 -136
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/WHEEL +0 -0
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/top_level.txt +0 -0
kailash/nodes/base.py
CHANGED
@@ -18,6 +18,7 @@ Key Components:
|
|
18
18
|
- NodeRegistry: Global registry for node discovery
|
19
19
|
"""
|
20
20
|
|
21
|
+
import inspect
|
21
22
|
import json
|
22
23
|
import logging
|
23
24
|
from abc import ABC, abstractmethod
|
@@ -67,7 +68,7 @@ class NodeMetadata(BaseModel):
|
|
67
68
|
|
68
69
|
|
69
70
|
class NodeParameter(BaseModel):
|
70
|
-
"""Definition of a node parameter.
|
71
|
+
"""Definition of a node parameter with enhanced auto-mapping capabilities.
|
71
72
|
|
72
73
|
This class defines the schema for node inputs and outputs, providing:
|
73
74
|
|
@@ -75,12 +76,20 @@ class NodeParameter(BaseModel):
|
|
75
76
|
2. Default values for optional parameters
|
76
77
|
3. Documentation for users
|
77
78
|
4. Requirements specification
|
79
|
+
5. Auto-mapping from workflow connections (NEW)
|
80
|
+
|
81
|
+
Enhanced Features (v0.2.0):
|
82
|
+
- auto_map_from: Alternative parameter names for flexible mapping
|
83
|
+
- auto_map_primary: Designates primary input for automatic data routing
|
84
|
+
- workflow_alias: Preferred name in workflow connections
|
85
|
+
- These features enable robust parameter resolution across all node types
|
78
86
|
|
79
87
|
Design Purpose:
|
80
88
|
- Enables static analysis of workflow connections
|
81
89
|
- Provides runtime validation of data types
|
82
90
|
- Supports automatic UI generation for node configuration
|
83
91
|
- Facilitates workflow validation before execution
|
92
|
+
- Resolves parameter mapping issues between workflow data and node inputs
|
84
93
|
|
85
94
|
Upstream usage:
|
86
95
|
- Node.get_parameters(): Returns dict of parameters
|
@@ -88,7 +97,7 @@ class NodeParameter(BaseModel):
|
|
88
97
|
|
89
98
|
Downstream consumers:
|
90
99
|
- Node._validate_config(): Validates configuration against parameters
|
91
|
-
- Node.validate_inputs(): Validates runtime inputs
|
100
|
+
- Node.validate_inputs(): Validates runtime inputs with auto-mapping
|
92
101
|
- Workflow.connect(): Validates connections between nodes
|
93
102
|
- WorkflowExporter: Exports parameter schemas
|
94
103
|
"""
|
@@ -99,6 +108,17 @@ class NodeParameter(BaseModel):
|
|
99
108
|
default: Any = None
|
100
109
|
description: str = ""
|
101
110
|
|
111
|
+
# Enhanced auto-mapping capabilities
|
112
|
+
auto_map_from: list[str] = Field(
|
113
|
+
default_factory=list, description="Alternative parameter names for auto-mapping"
|
114
|
+
)
|
115
|
+
auto_map_primary: bool = Field(
|
116
|
+
default=False, description="Use as primary input for automatic data routing"
|
117
|
+
)
|
118
|
+
workflow_alias: str = Field(
|
119
|
+
default="", description="Preferred name in workflow connections"
|
120
|
+
)
|
121
|
+
|
102
122
|
|
103
123
|
class Node(ABC):
|
104
124
|
"""Base class for all nodes in the Kailash system.
|
@@ -311,7 +331,6 @@ class Node(ABC):
|
|
311
331
|
"""
|
312
332
|
return {}
|
313
333
|
|
314
|
-
@abstractmethod
|
315
334
|
def run(self, **kwargs) -> dict[str, Any]:
|
316
335
|
"""Execute the node's logic.
|
317
336
|
|
@@ -360,6 +379,63 @@ class Node(ABC):
|
|
360
379
|
- LocalRuntime: During workflow execution
|
361
380
|
- TestRunner: During unit testing
|
362
381
|
"""
|
382
|
+
# Check if this node has implemented async_run
|
383
|
+
import inspect
|
384
|
+
|
385
|
+
# Get the actual async_run method from the instance's class
|
386
|
+
async_run_method = getattr(self.__class__, "async_run", None)
|
387
|
+
base_async_run = getattr(Node, "async_run", None)
|
388
|
+
|
389
|
+
# Check if async_run has been overridden
|
390
|
+
if async_run_method and async_run_method != base_async_run:
|
391
|
+
# This node has a custom async_run implementation
|
392
|
+
# Run it synchronously
|
393
|
+
import asyncio
|
394
|
+
|
395
|
+
try:
|
396
|
+
# Check if we're already in an event loop
|
397
|
+
loop = asyncio.get_running_loop()
|
398
|
+
|
399
|
+
# We're in an event loop - use nest_asyncio
|
400
|
+
import nest_asyncio
|
401
|
+
|
402
|
+
nest_asyncio.apply()
|
403
|
+
return asyncio.run(self.async_run(**kwargs))
|
404
|
+
|
405
|
+
except RuntimeError:
|
406
|
+
# No event loop running, we can use asyncio.run() directly
|
407
|
+
return asyncio.run(self.async_run(**kwargs))
|
408
|
+
else:
|
409
|
+
# This is a regular synchronous node - subclass should override this method
|
410
|
+
raise NotImplementedError(
|
411
|
+
f"Node '{self.__class__.__name__}' must implement either run() or async_run() method"
|
412
|
+
)
|
413
|
+
|
414
|
+
async def async_run(self, **kwargs) -> dict[str, Any]:
|
415
|
+
"""Asynchronous execution method for the node.
|
416
|
+
|
417
|
+
This method provides async execution support. By default, it calls
|
418
|
+
the synchronous run() method. Nodes can override this for true
|
419
|
+
async behavior.
|
420
|
+
|
421
|
+
Design Philosophy:
|
422
|
+
- Maintain backward compatibility with synchronous nodes
|
423
|
+
- Support both sync and async execution methods
|
424
|
+
- Provide clear error handling for async operations
|
425
|
+
- Enable efficient parallel execution in workflows
|
426
|
+
|
427
|
+
Args:
|
428
|
+
**kwargs: Validated input parameters matching get_parameters()
|
429
|
+
|
430
|
+
Returns:
|
431
|
+
Dictionary of outputs that will be validated and passed
|
432
|
+
to downstream nodes
|
433
|
+
|
434
|
+
Raises:
|
435
|
+
NodeExecutionError: If execution fails
|
436
|
+
"""
|
437
|
+
# Default implementation calls the synchronous run() method
|
438
|
+
return self.run(**kwargs)
|
363
439
|
|
364
440
|
def _validate_config(self):
|
365
441
|
"""Validate node configuration against defined parameters.
|
@@ -479,6 +555,7 @@ class Node(ABC):
|
|
479
555
|
- execute(): Before passing inputs to run()
|
480
556
|
- Workflow validation: During connection checks
|
481
557
|
"""
|
558
|
+
# Enhanced parameter resolution with auto-mapping
|
482
559
|
try:
|
483
560
|
params = self.get_parameters()
|
484
561
|
except Exception as e:
|
@@ -486,20 +563,123 @@ class Node(ABC):
|
|
486
563
|
f"Failed to get node parameters for validation: {e}"
|
487
564
|
) from e
|
488
565
|
|
489
|
-
|
566
|
+
# Phase 1: Resolve parameters using enhanced mapping
|
567
|
+
resolved = self._resolve_parameters(kwargs, params)
|
568
|
+
|
569
|
+
# Phase 2: Validate resolved parameters
|
570
|
+
validated = self._validate_resolved_parameters(resolved, params)
|
571
|
+
|
572
|
+
# Preserve special runtime parameters that are not in schema
|
573
|
+
special_params = ["context"]
|
574
|
+
for special_param in special_params:
|
575
|
+
if special_param in kwargs:
|
576
|
+
validated[special_param] = kwargs[special_param]
|
577
|
+
|
578
|
+
return validated
|
579
|
+
|
580
|
+
def _resolve_parameters(self, runtime_inputs: dict, params: dict) -> dict:
|
581
|
+
"""Enhanced parameter resolution with auto-mapping.
|
490
582
|
|
583
|
+
This method implements the core parameter mapping logic that resolves
|
584
|
+
workflow inputs to node parameters using multiple strategies:
|
585
|
+
|
586
|
+
1. Direct parameter matches (existing behavior)
|
587
|
+
2. Workflow alias mapping
|
588
|
+
3. Auto-mapping from alternative names
|
589
|
+
4. Primary input auto-detection
|
590
|
+
|
591
|
+
Args:
|
592
|
+
runtime_inputs: Inputs provided by workflow runtime
|
593
|
+
params: Node parameter definitions from get_parameters()
|
594
|
+
|
595
|
+
Returns:
|
596
|
+
Dict mapping parameter names to resolved values
|
597
|
+
"""
|
598
|
+
resolved = {}
|
599
|
+
used_inputs = set()
|
600
|
+
|
601
|
+
# Phase 1: Direct parameter matches (preserves existing behavior)
|
491
602
|
for param_name, param_def in params.items():
|
492
|
-
if
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
603
|
+
if param_name in runtime_inputs:
|
604
|
+
resolved[param_name] = runtime_inputs[param_name]
|
605
|
+
used_inputs.add(param_name)
|
606
|
+
if self.logger:
|
607
|
+
self.logger.debug(f"Direct match: {param_name}")
|
608
|
+
|
609
|
+
# Phase 2: Workflow alias resolution
|
610
|
+
for param_name, param_def in params.items():
|
611
|
+
if param_name in resolved:
|
612
|
+
continue
|
613
|
+
|
614
|
+
if param_def.workflow_alias and param_def.workflow_alias in runtime_inputs:
|
615
|
+
resolved[param_name] = runtime_inputs[param_def.workflow_alias]
|
616
|
+
used_inputs.add(param_def.workflow_alias)
|
617
|
+
if self.logger:
|
618
|
+
self.logger.debug(
|
619
|
+
f"Workflow alias match: {param_name} <- {param_def.workflow_alias}"
|
499
620
|
)
|
621
|
+
continue
|
500
622
|
|
501
|
-
|
502
|
-
|
623
|
+
# Phase 3: Auto-mapping from alternative names
|
624
|
+
for param_name, param_def in params.items():
|
625
|
+
if param_name in resolved:
|
626
|
+
continue
|
627
|
+
|
628
|
+
if param_def.auto_map_from:
|
629
|
+
for alt_name in param_def.auto_map_from:
|
630
|
+
if alt_name in runtime_inputs and alt_name not in used_inputs:
|
631
|
+
resolved[param_name] = runtime_inputs[alt_name]
|
632
|
+
used_inputs.add(alt_name)
|
633
|
+
if self.logger:
|
634
|
+
self.logger.debug(
|
635
|
+
f"Auto-map match: {param_name} <- {alt_name}"
|
636
|
+
)
|
637
|
+
break
|
638
|
+
|
639
|
+
# Phase 4: Primary input auto-mapping (for nodes like SwitchNode)
|
640
|
+
primary_params = [p for p in params.values() if p.auto_map_primary]
|
641
|
+
if primary_params and len(primary_params) == 1:
|
642
|
+
primary_param = primary_params[0]
|
643
|
+
if primary_param.name not in resolved:
|
644
|
+
# Find the main data input (usually the largest unused input)
|
645
|
+
remaining_inputs = {
|
646
|
+
k: v
|
647
|
+
for k, v in runtime_inputs.items()
|
648
|
+
if k not in used_inputs and not k.startswith("_")
|
649
|
+
}
|
650
|
+
if remaining_inputs:
|
651
|
+
# Use the input with the most substantial data as primary
|
652
|
+
main_input = max(
|
653
|
+
remaining_inputs.items(),
|
654
|
+
key=lambda x: len(str(x[1])) if x[1] is not None else 0,
|
655
|
+
)
|
656
|
+
resolved[primary_param.name] = main_input[1]
|
657
|
+
used_inputs.add(main_input[0])
|
658
|
+
if self.logger:
|
659
|
+
self.logger.debug(
|
660
|
+
f"Primary auto-map: {primary_param.name} <- {main_input[0]}"
|
661
|
+
)
|
662
|
+
|
663
|
+
return resolved
|
664
|
+
|
665
|
+
def _validate_resolved_parameters(self, resolved: dict, params: dict) -> dict:
|
666
|
+
"""Validate resolved parameters against their definitions.
|
667
|
+
|
668
|
+
Args:
|
669
|
+
resolved: Parameters resolved by _resolve_parameters
|
670
|
+
params: Node parameter definitions
|
671
|
+
|
672
|
+
Returns:
|
673
|
+
Dict of validated parameters with type conversions applied
|
674
|
+
|
675
|
+
Raises:
|
676
|
+
NodeValidationError: If validation fails
|
677
|
+
"""
|
678
|
+
validated = {}
|
679
|
+
|
680
|
+
for param_name, param_def in params.items():
|
681
|
+
if param_name in resolved:
|
682
|
+
value = resolved[param_name]
|
503
683
|
if value is None and not param_def.required:
|
504
684
|
continue
|
505
685
|
|
@@ -517,8 +697,49 @@ class Node(ABC):
|
|
517
697
|
else:
|
518
698
|
validated[param_name] = value
|
519
699
|
|
700
|
+
elif param_def.required:
|
701
|
+
if param_def.default is not None:
|
702
|
+
validated[param_name] = param_def.default
|
703
|
+
else:
|
704
|
+
# Enhanced error message with suggestions
|
705
|
+
available = list(resolved.keys()) if resolved else ["none"]
|
706
|
+
suggestions = self._suggest_parameter_mapping(
|
707
|
+
param_name, list(resolved.keys())
|
708
|
+
)
|
709
|
+
raise NodeValidationError(
|
710
|
+
f"Required parameter '{param_name}' not provided. "
|
711
|
+
f"Available resolved inputs: {available}. "
|
712
|
+
f"Mapping suggestions: {suggestions}. "
|
713
|
+
f"Description: {param_def.description or 'No description available'}"
|
714
|
+
)
|
715
|
+
|
520
716
|
return validated
|
521
717
|
|
718
|
+
def _suggest_parameter_mapping(
|
719
|
+
self, param_name: str, available: list[str]
|
720
|
+
) -> list[str]:
|
721
|
+
"""Suggest likely parameter mappings based on name similarity.
|
722
|
+
|
723
|
+
Args:
|
724
|
+
param_name: The parameter name we're trying to map
|
725
|
+
available: List of available input names
|
726
|
+
|
727
|
+
Returns:
|
728
|
+
List of suggested parameter names
|
729
|
+
"""
|
730
|
+
try:
|
731
|
+
import difflib
|
732
|
+
|
733
|
+
return difflib.get_close_matches(param_name, available, n=3, cutoff=0.3)
|
734
|
+
except ImportError:
|
735
|
+
# Fallback if difflib is not available
|
736
|
+
return [
|
737
|
+
name
|
738
|
+
for name in available
|
739
|
+
if param_name.lower() in name.lower()
|
740
|
+
or name.lower() in param_name.lower()
|
741
|
+
]
|
742
|
+
|
522
743
|
def validate_outputs(self, outputs: dict[str, Any]) -> dict[str, Any]:
|
523
744
|
"""Validate outputs against schema and JSON-serializability.
|
524
745
|
|
@@ -752,6 +973,67 @@ class Node(ABC):
|
|
752
973
|
f"Node '{self.id}' execution failed: {type(e).__name__}: {e}"
|
753
974
|
) from e
|
754
975
|
|
976
|
+
async def execute_async(self, **runtime_inputs) -> dict[str, Any]:
|
977
|
+
"""Execute the node asynchronously with validation and error handling.
|
978
|
+
|
979
|
+
This is the async version of execute() that provides the same
|
980
|
+
validation and error handling but allows for async execution.
|
981
|
+
|
982
|
+
Execution flow:
|
983
|
+
1. Logs execution start
|
984
|
+
2. Validates inputs against parameter schema
|
985
|
+
3. Calls async_run() with validated inputs
|
986
|
+
4. Validates outputs are JSON-serializable
|
987
|
+
5. Logs execution time
|
988
|
+
6. Returns validated outputs
|
989
|
+
|
990
|
+
Returns:
|
991
|
+
Dictionary of validated outputs from async_run()
|
992
|
+
|
993
|
+
Raises:
|
994
|
+
NodeExecutionError: If execution fails
|
995
|
+
NodeValidationError: If input/output validation fails
|
996
|
+
"""
|
997
|
+
from datetime import UTC, datetime
|
998
|
+
|
999
|
+
start_time = datetime.now(UTC)
|
1000
|
+
self.logger.info(f"Starting async execution of node {self.id}")
|
1001
|
+
|
1002
|
+
try:
|
1003
|
+
# Merge config and runtime inputs
|
1004
|
+
merged_inputs = {**self.config, **runtime_inputs}
|
1005
|
+
|
1006
|
+
# Validate inputs
|
1007
|
+
validated_inputs = self.validate_inputs(**merged_inputs)
|
1008
|
+
self.logger.debug(f"Validated inputs for {self.id}: {validated_inputs}")
|
1009
|
+
|
1010
|
+
# Execute node logic asynchronously
|
1011
|
+
outputs = await self.async_run(**validated_inputs)
|
1012
|
+
|
1013
|
+
# Validate outputs
|
1014
|
+
validated_outputs = self.validate_outputs(outputs)
|
1015
|
+
|
1016
|
+
execution_time = (datetime.now(UTC) - start_time).total_seconds()
|
1017
|
+
self.logger.info(
|
1018
|
+
f"Node {self.id} executed successfully (async) in {execution_time:.3f}s"
|
1019
|
+
)
|
1020
|
+
return validated_outputs
|
1021
|
+
|
1022
|
+
except NodeValidationError:
|
1023
|
+
# Re-raise validation errors as-is
|
1024
|
+
raise
|
1025
|
+
except NodeExecutionError:
|
1026
|
+
# Re-raise execution errors as-is
|
1027
|
+
raise
|
1028
|
+
except Exception as e:
|
1029
|
+
# Wrap unexpected errors
|
1030
|
+
self.logger.error(
|
1031
|
+
f"Node {self.id} async execution failed: {e}", exc_info=True
|
1032
|
+
)
|
1033
|
+
raise NodeExecutionError(
|
1034
|
+
f"Node '{self.id}' async execution failed: {type(e).__name__}: {e}"
|
1035
|
+
) from e
|
1036
|
+
|
755
1037
|
def to_dict(self) -> dict[str, Any]:
|
756
1038
|
"""Convert node to dictionary representation.
|
757
1039
|
|
@@ -929,6 +1211,9 @@ class NodeRegistry:
|
|
929
1211
|
f"Cannot register {node_class.__name__}: must be a subclass of Node"
|
930
1212
|
)
|
931
1213
|
|
1214
|
+
# Validate constructor signature (Core SDK improvement)
|
1215
|
+
cls._validate_node_constructor(node_class)
|
1216
|
+
|
932
1217
|
node_name = alias or node_class.__name__
|
933
1218
|
|
934
1219
|
if node_name in cls._nodes:
|
@@ -937,6 +1222,52 @@ class NodeRegistry:
|
|
937
1222
|
cls._nodes[node_name] = node_class
|
938
1223
|
logging.info(f"Registered node '{node_name}'")
|
939
1224
|
|
1225
|
+
@classmethod
|
1226
|
+
def _validate_node_constructor(cls, node_class: type[Node]):
|
1227
|
+
"""Validate that node constructor follows SDK patterns.
|
1228
|
+
|
1229
|
+
This is a core SDK improvement to ensure all nodes have consistent
|
1230
|
+
constructor signatures that work with WorkflowBuilder.from_dict().
|
1231
|
+
|
1232
|
+
Validates that the node constructor either:
|
1233
|
+
1. Accepts 'name' parameter (like PythonCodeNode)
|
1234
|
+
2. Accepts 'id' parameter (traditional pattern)
|
1235
|
+
3. Uses **kwargs to accept both
|
1236
|
+
|
1237
|
+
Args:
|
1238
|
+
node_class: Node class to validate
|
1239
|
+
|
1240
|
+
Raises:
|
1241
|
+
NodeConfigurationError: If constructor signature is incompatible
|
1242
|
+
"""
|
1243
|
+
try:
|
1244
|
+
sig = inspect.signature(node_class.__init__)
|
1245
|
+
params = list(sig.parameters.keys())
|
1246
|
+
|
1247
|
+
# Skip 'self' parameter
|
1248
|
+
if "self" in params:
|
1249
|
+
params.remove("self")
|
1250
|
+
|
1251
|
+
# Check if constructor accepts required parameters
|
1252
|
+
has_name = "name" in params
|
1253
|
+
has_id = "id" in params
|
1254
|
+
has_kwargs = any(
|
1255
|
+
param.kind == param.VAR_KEYWORD for param in sig.parameters.values()
|
1256
|
+
)
|
1257
|
+
|
1258
|
+
if not (has_name or has_id or has_kwargs):
|
1259
|
+
logging.warning(
|
1260
|
+
f"Node {node_class.__name__} constructor may not work with WorkflowBuilder.from_dict(). "
|
1261
|
+
f"Constructor should accept 'name', 'id', or **kwargs parameter. "
|
1262
|
+
f"Current parameters: {params}"
|
1263
|
+
)
|
1264
|
+
|
1265
|
+
except Exception as e:
|
1266
|
+
# Don't fail registration for signature inspection issues
|
1267
|
+
logging.warning(
|
1268
|
+
f"Could not validate constructor for {node_class.__name__}: {e}"
|
1269
|
+
)
|
1270
|
+
|
940
1271
|
@classmethod
|
941
1272
|
def get(cls, node_name: str) -> type[Node]:
|
942
1273
|
"""Get a registered node class by name.
|
@@ -21,7 +21,8 @@ Example usage:
|
|
21
21
|
... "data": NodeParameter(name="data", type=list, required=True)
|
22
22
|
... }
|
23
23
|
...
|
24
|
-
... def run(self,
|
24
|
+
... def run(self, **kwargs):
|
25
|
+
... context = kwargs.get("context", {})
|
25
26
|
... iteration = self.get_iteration(context)
|
26
27
|
... is_first = self.is_first_iteration(context)
|
27
28
|
... prev_results = self.get_previous_state(context)
|
@@ -87,7 +88,8 @@ class CycleAwareNode(Node):
|
|
87
88
|
|
88
89
|
Examples:
|
89
90
|
>>> class QualityImproverNode(CycleAwareNode):
|
90
|
-
... def run(self,
|
91
|
+
... def run(self, **kwargs):
|
92
|
+
... context = kwargs.get("context", {})
|
91
93
|
... iteration = self.get_iteration(context)
|
92
94
|
... quality = kwargs.get("quality", 0.0)
|
93
95
|
...
|
kailash/nodes/base_with_acl.py
CHANGED
@@ -224,7 +224,7 @@ class AsyncNodeWithAccessControl(AsyncNode):
|
|
224
224
|
self._fallback_node = config.get("fallback_node", None)
|
225
225
|
self._mask_output_fields = config.get("mask_output_fields", [])
|
226
226
|
|
227
|
-
async def
|
227
|
+
async def async_run(self, **inputs) -> Any:
|
228
228
|
"""Async execution with optional access control"""
|
229
229
|
runtime_context = inputs.pop("_runtime_context", None)
|
230
230
|
user_context = inputs.pop("_user_context", None)
|