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.
Files changed (146) hide show
  1. kailash/__init__.py +33 -1
  2. kailash/access_control/__init__.py +129 -0
  3. kailash/access_control/managers.py +461 -0
  4. kailash/access_control/rule_evaluators.py +467 -0
  5. kailash/access_control_abac.py +825 -0
  6. kailash/config/__init__.py +27 -0
  7. kailash/config/database_config.py +359 -0
  8. kailash/database/__init__.py +28 -0
  9. kailash/database/execution_pipeline.py +499 -0
  10. kailash/middleware/__init__.py +306 -0
  11. kailash/middleware/auth/__init__.py +33 -0
  12. kailash/middleware/auth/access_control.py +436 -0
  13. kailash/middleware/auth/auth_manager.py +422 -0
  14. kailash/middleware/auth/jwt_auth.py +477 -0
  15. kailash/middleware/auth/kailash_jwt_auth.py +616 -0
  16. kailash/middleware/communication/__init__.py +37 -0
  17. kailash/middleware/communication/ai_chat.py +989 -0
  18. kailash/middleware/communication/api_gateway.py +802 -0
  19. kailash/middleware/communication/events.py +470 -0
  20. kailash/middleware/communication/realtime.py +710 -0
  21. kailash/middleware/core/__init__.py +21 -0
  22. kailash/middleware/core/agent_ui.py +890 -0
  23. kailash/middleware/core/schema.py +643 -0
  24. kailash/middleware/core/workflows.py +396 -0
  25. kailash/middleware/database/__init__.py +63 -0
  26. kailash/middleware/database/base.py +113 -0
  27. kailash/middleware/database/base_models.py +525 -0
  28. kailash/middleware/database/enums.py +106 -0
  29. kailash/middleware/database/migrations.py +12 -0
  30. kailash/{api/database.py → middleware/database/models.py} +183 -291
  31. kailash/middleware/database/repositories.py +685 -0
  32. kailash/middleware/database/session_manager.py +19 -0
  33. kailash/middleware/mcp/__init__.py +38 -0
  34. kailash/middleware/mcp/client_integration.py +585 -0
  35. kailash/middleware/mcp/enhanced_server.py +576 -0
  36. kailash/nodes/__init__.py +25 -3
  37. kailash/nodes/admin/__init__.py +35 -0
  38. kailash/nodes/admin/audit_log.py +794 -0
  39. kailash/nodes/admin/permission_check.py +864 -0
  40. kailash/nodes/admin/role_management.py +823 -0
  41. kailash/nodes/admin/security_event.py +1519 -0
  42. kailash/nodes/admin/user_management.py +944 -0
  43. kailash/nodes/ai/a2a.py +24 -7
  44. kailash/nodes/ai/ai_providers.py +1 -0
  45. kailash/nodes/ai/embedding_generator.py +11 -11
  46. kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
  47. kailash/nodes/ai/llm_agent.py +407 -2
  48. kailash/nodes/ai/self_organizing.py +85 -10
  49. kailash/nodes/api/auth.py +287 -6
  50. kailash/nodes/api/rest.py +151 -0
  51. kailash/nodes/auth/__init__.py +17 -0
  52. kailash/nodes/auth/directory_integration.py +1228 -0
  53. kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
  54. kailash/nodes/auth/mfa.py +2338 -0
  55. kailash/nodes/auth/risk_assessment.py +872 -0
  56. kailash/nodes/auth/session_management.py +1093 -0
  57. kailash/nodes/auth/sso.py +1040 -0
  58. kailash/nodes/base.py +344 -13
  59. kailash/nodes/base_cycle_aware.py +4 -2
  60. kailash/nodes/base_with_acl.py +1 -1
  61. kailash/nodes/code/python.py +293 -12
  62. kailash/nodes/compliance/__init__.py +9 -0
  63. kailash/nodes/compliance/data_retention.py +1888 -0
  64. kailash/nodes/compliance/gdpr.py +2004 -0
  65. kailash/nodes/data/__init__.py +22 -2
  66. kailash/nodes/data/async_connection.py +469 -0
  67. kailash/nodes/data/async_sql.py +757 -0
  68. kailash/nodes/data/async_vector.py +598 -0
  69. kailash/nodes/data/readers.py +767 -0
  70. kailash/nodes/data/retrieval.py +360 -1
  71. kailash/nodes/data/sharepoint_graph.py +397 -21
  72. kailash/nodes/data/sql.py +94 -5
  73. kailash/nodes/data/streaming.py +68 -8
  74. kailash/nodes/data/vector_db.py +54 -4
  75. kailash/nodes/enterprise/__init__.py +13 -0
  76. kailash/nodes/enterprise/batch_processor.py +741 -0
  77. kailash/nodes/enterprise/data_lineage.py +497 -0
  78. kailash/nodes/logic/convergence.py +31 -9
  79. kailash/nodes/logic/operations.py +14 -3
  80. kailash/nodes/mixins/__init__.py +8 -0
  81. kailash/nodes/mixins/event_emitter.py +201 -0
  82. kailash/nodes/mixins/mcp.py +9 -4
  83. kailash/nodes/mixins/security.py +165 -0
  84. kailash/nodes/monitoring/__init__.py +7 -0
  85. kailash/nodes/monitoring/performance_benchmark.py +2497 -0
  86. kailash/nodes/rag/__init__.py +284 -0
  87. kailash/nodes/rag/advanced.py +1615 -0
  88. kailash/nodes/rag/agentic.py +773 -0
  89. kailash/nodes/rag/conversational.py +999 -0
  90. kailash/nodes/rag/evaluation.py +875 -0
  91. kailash/nodes/rag/federated.py +1188 -0
  92. kailash/nodes/rag/graph.py +721 -0
  93. kailash/nodes/rag/multimodal.py +671 -0
  94. kailash/nodes/rag/optimized.py +933 -0
  95. kailash/nodes/rag/privacy.py +1059 -0
  96. kailash/nodes/rag/query_processing.py +1335 -0
  97. kailash/nodes/rag/realtime.py +764 -0
  98. kailash/nodes/rag/registry.py +547 -0
  99. kailash/nodes/rag/router.py +837 -0
  100. kailash/nodes/rag/similarity.py +1854 -0
  101. kailash/nodes/rag/strategies.py +566 -0
  102. kailash/nodes/rag/workflows.py +575 -0
  103. kailash/nodes/security/__init__.py +19 -0
  104. kailash/nodes/security/abac_evaluator.py +1411 -0
  105. kailash/nodes/security/audit_log.py +91 -0
  106. kailash/nodes/security/behavior_analysis.py +1893 -0
  107. kailash/nodes/security/credential_manager.py +401 -0
  108. kailash/nodes/security/rotating_credentials.py +760 -0
  109. kailash/nodes/security/security_event.py +132 -0
  110. kailash/nodes/security/threat_detection.py +1103 -0
  111. kailash/nodes/testing/__init__.py +9 -0
  112. kailash/nodes/testing/credential_testing.py +499 -0
  113. kailash/nodes/transform/__init__.py +10 -2
  114. kailash/nodes/transform/chunkers.py +592 -1
  115. kailash/nodes/transform/processors.py +484 -14
  116. kailash/nodes/validation.py +321 -0
  117. kailash/runtime/access_controlled.py +1 -1
  118. kailash/runtime/async_local.py +41 -7
  119. kailash/runtime/docker.py +1 -1
  120. kailash/runtime/local.py +474 -55
  121. kailash/runtime/parallel.py +1 -1
  122. kailash/runtime/parallel_cyclic.py +1 -1
  123. kailash/runtime/testing.py +210 -2
  124. kailash/utils/migrations/__init__.py +25 -0
  125. kailash/utils/migrations/generator.py +433 -0
  126. kailash/utils/migrations/models.py +231 -0
  127. kailash/utils/migrations/runner.py +489 -0
  128. kailash/utils/secure_logging.py +342 -0
  129. kailash/workflow/__init__.py +16 -0
  130. kailash/workflow/cyclic_runner.py +3 -4
  131. kailash/workflow/graph.py +70 -2
  132. kailash/workflow/resilience.py +249 -0
  133. kailash/workflow/templates.py +726 -0
  134. {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/METADATA +253 -20
  135. kailash-0.4.0.dist-info/RECORD +223 -0
  136. kailash/api/__init__.py +0 -17
  137. kailash/api/__main__.py +0 -6
  138. kailash/api/studio_secure.py +0 -893
  139. kailash/mcp/__main__.py +0 -13
  140. kailash/mcp/server_new.py +0 -336
  141. kailash/mcp/servers/__init__.py +0 -12
  142. kailash-0.3.1.dist-info/RECORD +0 -136
  143. {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/WHEEL +0 -0
  144. {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/entry_points.txt +0 -0
  145. {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/licenses/LICENSE +0 -0
  146. {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
- validated = {}
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 param_def.required and param_name not in kwargs:
493
- if param_def.default is not None:
494
- validated[param_name] = param_def.default
495
- else:
496
- raise NodeValidationError(
497
- f"Required input '{param_name}' not provided. "
498
- f"Description: {param_def.description or 'No description available'}"
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
- if param_name in kwargs:
502
- value = kwargs[param_name]
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, context, **kwargs):
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, context, **kwargs):
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
  ...
@@ -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 run(self, **inputs) -> Any:
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)