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
@@ -0,0 +1,342 @@
1
+ """Secure logging utilities for masking sensitive data.
2
+
3
+ This module provides mixins and utilities for automatically detecting and
4
+ masking PII, credentials, and other sensitive information in logs.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import re
10
+ from functools import wraps
11
+ from typing import Any, Dict, List, Optional, Pattern, Set, Union
12
+
13
+
14
+ class SecureLoggingPatterns:
15
+ """Patterns for detecting sensitive data."""
16
+
17
+ # Credit card patterns
18
+ CREDIT_CARD = re.compile(r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b")
19
+
20
+ # SSN patterns
21
+ SSN = re.compile(r"\b\d{3}-\d{2}-\d{4}\b|\b\d{9}\b")
22
+
23
+ # Email pattern
24
+ EMAIL = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
25
+
26
+ # Phone patterns
27
+ PHONE = re.compile(
28
+ r"\b(?:\+?1[-.]?)?\(?([0-9]{3})\)?[-.]?([0-9]{3})[-.]?([0-9]{4})\b"
29
+ )
30
+
31
+ # API key patterns
32
+ API_KEY_PATTERNS = [
33
+ re.compile(r"sk-[a-zA-Z0-9]{48}"), # OpenAI
34
+ re.compile(r"AIza[0-9A-Za-z\-_]{35}"), # Google
35
+ re.compile(r"ghp_[a-zA-Z0-9]{36}"), # GitHub
36
+ re.compile(r"[a-zA-Z0-9]{32}"), # Generic 32-char
37
+ ]
38
+
39
+ # Password in various formats
40
+ PASSWORD_PATTERNS = [
41
+ re.compile(r'["\']?password["\']?\s*[:=]\s*["\']?([^"\']+)["\']?', re.I),
42
+ re.compile(r'["\']?pwd["\']?\s*[:=]\s*["\']?([^"\']+)["\']?', re.I),
43
+ re.compile(r'["\']?pass["\']?\s*[:=]\s*["\']?([^"\']+)["\']?', re.I),
44
+ ]
45
+
46
+ # Token patterns
47
+ TOKEN_PATTERNS = [
48
+ re.compile(r'["\']?token["\']?\s*[:=]\s*["\']?([^"\']+)["\']?', re.I),
49
+ re.compile(r'["\']?api_key["\']?\s*[:=]\s*["\']?([^"\']+)["\']?', re.I),
50
+ re.compile(r'["\']?secret["\']?\s*[:=]\s*["\']?([^"\']+)["\']?', re.I),
51
+ ]
52
+
53
+ # Common PII field names
54
+ PII_FIELD_NAMES = {
55
+ "ssn",
56
+ "social_security",
57
+ "social_security_number",
58
+ "credit_card",
59
+ "card_number",
60
+ "cc_number",
61
+ "password",
62
+ "pwd",
63
+ "pass",
64
+ "passwd",
65
+ "token",
66
+ "api_key",
67
+ "apikey",
68
+ "secret",
69
+ "private_key",
70
+ "email",
71
+ "email_address",
72
+ "phone",
73
+ "phone_number",
74
+ "address",
75
+ "street_address",
76
+ "home_address",
77
+ "date_of_birth",
78
+ "dob",
79
+ "birthdate",
80
+ "driver_license",
81
+ "license_number",
82
+ "passport",
83
+ "passport_number",
84
+ "bank_account",
85
+ "account_number",
86
+ "routing_number",
87
+ }
88
+
89
+
90
+ class SecureLogger:
91
+ """Logger that automatically masks sensitive data."""
92
+
93
+ def __init__(
94
+ self,
95
+ name: str,
96
+ mask_char: str = "*",
97
+ mask_length: int = 8,
98
+ custom_patterns: Optional[List[Pattern]] = None,
99
+ custom_fields: Optional[Set[str]] = None,
100
+ ):
101
+ """
102
+ Initialize secure logger.
103
+
104
+ Args:
105
+ name: Logger name
106
+ mask_char: Character to use for masking
107
+ mask_length: Fixed length for masks (0 = preserve length)
108
+ custom_patterns: Additional regex patterns to mask
109
+ custom_fields: Additional field names to mask
110
+ """
111
+ self.logger = logging.getLogger(name)
112
+ self.mask_char = mask_char
113
+ self.mask_length = mask_length
114
+ self.custom_patterns = custom_patterns or []
115
+ self.custom_fields = custom_fields or set()
116
+
117
+ def _mask_value(self, value: str, preserve_partial: bool = True) -> str:
118
+ """Mask a sensitive value."""
119
+ if not value:
120
+ return value
121
+
122
+ if self.mask_length > 0:
123
+ # Fixed length mask
124
+ return self.mask_char * self.mask_length
125
+ elif preserve_partial and len(value) > 8:
126
+ # Preserve first 2 and last 2 chars
127
+ return value[:2] + self.mask_char * (len(value) - 4) + value[-2:]
128
+ else:
129
+ # Full mask
130
+ return self.mask_char * len(value)
131
+
132
+ def _mask_dict(self, data: Dict[str, Any]) -> Dict[str, Any]:
133
+ """Recursively mask sensitive fields in dictionary."""
134
+ masked = {}
135
+
136
+ for key, value in data.items():
137
+ # Check if field name indicates sensitive data
138
+ if (
139
+ key.lower() in SecureLoggingPatterns.PII_FIELD_NAMES
140
+ or key.lower() in self.custom_fields
141
+ ):
142
+ masked[key] = (
143
+ self._mask_value(str(value)) if value is not None else None
144
+ )
145
+ elif isinstance(value, dict):
146
+ masked[key] = self._mask_dict(value)
147
+ elif isinstance(value, list):
148
+ masked[key] = [
149
+ self._mask_dict(item) if isinstance(item, dict) else item
150
+ for item in value
151
+ ]
152
+ else:
153
+ masked[key] = value
154
+
155
+ return masked
156
+
157
+ def _mask_string(self, text: str) -> str:
158
+ """Mask sensitive patterns in string."""
159
+ # Credit cards
160
+ text = SecureLoggingPatterns.CREDIT_CARD.sub(
161
+ lambda m: self._mask_value(m.group(), preserve_partial=True), text
162
+ )
163
+
164
+ # SSNs
165
+ text = SecureLoggingPatterns.SSN.sub(
166
+ lambda m: self._mask_value(m.group(), preserve_partial=False), text
167
+ )
168
+
169
+ # Emails - preserve domain
170
+ text = SecureLoggingPatterns.EMAIL.sub(
171
+ lambda m: self._mask_email(m.group()), text
172
+ )
173
+
174
+ # Phone numbers
175
+ text = SecureLoggingPatterns.PHONE.sub(
176
+ lambda m: self._mask_value(m.group(), preserve_partial=True), text
177
+ )
178
+
179
+ # API keys
180
+ for pattern in SecureLoggingPatterns.API_KEY_PATTERNS:
181
+ text = pattern.sub(lambda m: self._mask_value(m.group()), text)
182
+
183
+ # Passwords and tokens
184
+ for pattern in (
185
+ SecureLoggingPatterns.PASSWORD_PATTERNS
186
+ + SecureLoggingPatterns.TOKEN_PATTERNS
187
+ ):
188
+ text = pattern.sub(
189
+ lambda m: m.group().replace(m.group(1), self._mask_value(m.group(1))),
190
+ text,
191
+ )
192
+
193
+ # Custom patterns
194
+ for pattern in self.custom_patterns:
195
+ text = pattern.sub(lambda m: self._mask_value(m.group()), text)
196
+
197
+ return text
198
+
199
+ def _mask_email(self, email: str) -> str:
200
+ """Mask email preserving domain."""
201
+ if "@" in email:
202
+ local, domain = email.split("@", 1)
203
+ return self._mask_value(local, preserve_partial=True) + "@" + domain
204
+ return self._mask_value(email)
205
+
206
+ def _mask_data(self, data: Any) -> Any:
207
+ """Mask sensitive data in any format."""
208
+ if isinstance(data, str):
209
+ return self._mask_string(data)
210
+ elif isinstance(data, dict):
211
+ return self._mask_dict(data)
212
+ elif isinstance(data, (list, tuple)):
213
+ return [self._mask_data(item) for item in data]
214
+ else:
215
+ return data
216
+
217
+ def debug(self, msg: str, *args, **kwargs):
218
+ """Log debug with masking."""
219
+ masked_msg = self._mask_string(msg % args if args else msg)
220
+ masked_kwargs = self._mask_dict(kwargs)
221
+ self.logger.debug(masked_msg, **masked_kwargs)
222
+
223
+ def info(self, msg: str, *args, **kwargs):
224
+ """Log info with masking."""
225
+ masked_msg = self._mask_string(msg % args if args else msg)
226
+ masked_kwargs = self._mask_dict(kwargs)
227
+ self.logger.info(masked_msg, **masked_kwargs)
228
+
229
+ def warning(self, msg: str, *args, **kwargs):
230
+ """Log warning with masking."""
231
+ masked_msg = self._mask_string(msg % args if args else msg)
232
+ masked_kwargs = self._mask_dict(kwargs)
233
+ self.logger.warning(masked_msg, **masked_kwargs)
234
+
235
+ def error(self, msg: str, *args, **kwargs):
236
+ """Log error with masking."""
237
+ masked_msg = self._mask_string(msg % args if args else msg)
238
+ masked_kwargs = self._mask_dict(kwargs)
239
+ self.logger.error(masked_msg, **masked_kwargs)
240
+
241
+
242
+ class SecureLoggingMixin:
243
+ """Mixin to add secure logging to any class."""
244
+
245
+ def __init__(self, *args, **kwargs):
246
+ """Initialize with secure logger."""
247
+ super().__init__(*args, **kwargs)
248
+ self._secure_logger = SecureLogger(
249
+ name=f"{self.__class__.__module__}.{self.__class__.__name__}",
250
+ custom_fields=getattr(self, "_sensitive_fields", set()),
251
+ )
252
+
253
+ def log_debug(self, msg: str, data: Optional[Dict[str, Any]] = None):
254
+ """Log debug with automatic masking."""
255
+ if data:
256
+ self._secure_logger.debug(
257
+ f"{msg}: {json.dumps(self._secure_logger._mask_data(data))}"
258
+ )
259
+ else:
260
+ self._secure_logger.debug(msg)
261
+
262
+ def log_info(self, msg: str, data: Optional[Dict[str, Any]] = None):
263
+ """Log info with automatic masking."""
264
+ if data:
265
+ self._secure_logger.info(
266
+ f"{msg}: {json.dumps(self._secure_logger._mask_data(data))}"
267
+ )
268
+ else:
269
+ self._secure_logger.info(msg)
270
+
271
+ def log_error(
272
+ self,
273
+ msg: str,
274
+ error: Optional[Exception] = None,
275
+ data: Optional[Dict[str, Any]] = None,
276
+ ):
277
+ """Log error with automatic masking."""
278
+ error_msg = f"{msg}: {str(error)}" if error else msg
279
+ if data:
280
+ self._secure_logger.error(
281
+ f"{error_msg}: {json.dumps(self._secure_logger._mask_data(data))}"
282
+ )
283
+ else:
284
+ self._secure_logger.error(error_msg)
285
+
286
+
287
+ def secure_log(mask_params: Optional[List[str]] = None):
288
+ """Decorator for secure logging of function calls."""
289
+
290
+ def decorator(func):
291
+ @wraps(func)
292
+ def wrapper(*args, **kwargs):
293
+ logger = SecureLogger(f"{func.__module__}.{func.__name__}")
294
+
295
+ # Mask specified parameters
296
+ masked_kwargs = {}
297
+ for key, value in kwargs.items():
298
+ if mask_params and key in mask_params:
299
+ masked_kwargs[key] = logger._mask_value(str(value))
300
+ else:
301
+ masked_kwargs[key] = logger._mask_data(value)
302
+
303
+ logger.debug(f"Calling {func.__name__} with args: {masked_kwargs}")
304
+
305
+ try:
306
+ result = func(*args, **kwargs)
307
+ logger.debug(f"{func.__name__} completed successfully")
308
+ return result
309
+ except Exception as e:
310
+ logger.error(f"{func.__name__} failed: {str(e)}")
311
+ raise
312
+
313
+ return wrapper
314
+
315
+ return decorator
316
+
317
+
318
+ def apply_secure_logging_to_node(node_class):
319
+ """Decorator to add secure logging to a node class."""
320
+
321
+ # Create new class that inherits from both
322
+ class SecureNode(SecureLoggingMixin, node_class):
323
+ """Node with secure logging enabled."""
324
+
325
+ def run(self, **inputs):
326
+ """Run with secure logging."""
327
+ self.log_debug("Node execution started", inputs)
328
+
329
+ try:
330
+ result = super().run(**inputs)
331
+ self.log_debug("Node execution completed")
332
+ return result
333
+ except Exception as e:
334
+ self.log_error("Node execution failed", e, inputs)
335
+ raise
336
+
337
+ # Preserve original class name and module
338
+ SecureNode.__name__ = node_class.__name__
339
+ SecureNode.__module__ = node_class.__module__
340
+ SecureNode.__qualname__ = node_class.__qualname__
341
+
342
+ return SecureNode
@@ -12,6 +12,15 @@ from kailash.workflow.cycle_debugger import (
12
12
  from kailash.workflow.cycle_profiler import CycleProfiler, PerformanceMetrics
13
13
  from kailash.workflow.graph import Connection, NodeInstance, Workflow
14
14
  from kailash.workflow.mermaid_visualizer import MermaidVisualizer
15
+ from kailash.workflow.resilience import (
16
+ CircuitBreakerConfig,
17
+ RetryPolicy,
18
+ RetryStrategy,
19
+ WorkflowResilience,
20
+ apply_resilience_to_workflow,
21
+ )
22
+ from kailash.workflow.templates import BusinessWorkflowTemplates
23
+ from kailash.workflow.templates import CycleTemplates as WorkflowCycleTemplates
15
24
  from kailash.workflow.visualization import WorkflowVisualizer
16
25
 
17
26
  __all__ = [
@@ -30,4 +39,11 @@ __all__ = [
30
39
  "CycleProfiler",
31
40
  "PerformanceMetrics",
32
41
  "CycleAnalyzer",
42
+ "RetryStrategy",
43
+ "RetryPolicy",
44
+ "CircuitBreakerConfig",
45
+ "WorkflowResilience",
46
+ "apply_resilience_to_workflow",
47
+ "WorkflowCycleTemplates",
48
+ "BusinessWorkflowTemplates",
33
49
  ]
@@ -698,10 +698,9 @@ class CyclicWorkflowExecutor:
698
698
  "iteration": cycle_state.iteration,
699
699
  "elapsed_time": cycle_state.elapsed_time,
700
700
  }
701
- # Only add node_state if it's not None to avoid security validation errors
701
+ # Always include node_state in context, defaulting to empty dict
702
702
  node_state = cycle_state.get_node_state(node_id)
703
- if node_state is not None:
704
- cycle_context["node_state"] = node_state
703
+ cycle_context["node_state"] = node_state if node_state is not None else {}
705
704
  context["cycle"] = cycle_context
706
705
 
707
706
  # Recursively filter None values from context to avoid security validation errors
@@ -778,7 +777,7 @@ class CyclicWorkflowExecutor:
778
777
 
779
778
  try:
780
779
  with collector.collect(node_id=node_id) as metrics_context:
781
- result = node.run(context=context, **merged_inputs)
780
+ result = node.execute(context=context, **merged_inputs)
782
781
 
783
782
  # Get performance metrics
784
783
  performance_metrics = metrics_context.result()
kailash/workflow/graph.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Workflow DAG implementation for the Kailash SDK."""
2
2
 
3
+ import inspect
3
4
  import json
4
5
  import logging
5
6
  import uuid
@@ -128,6 +129,71 @@ class Workflow:
128
129
 
129
130
  logger.info(f"Created workflow '{name}' (ID: {workflow_id})")
130
131
 
132
+ def _create_node_instance(
133
+ self, node_class: type, node_id: str, config: dict
134
+ ) -> Node:
135
+ """Create a node instance with proper parameter mapping.
136
+
137
+ Handles the inconsistency between nodes that expect 'name' vs 'id' parameters.
138
+ This is a core SDK improvement to standardize node constructor patterns.
139
+
140
+ Args:
141
+ node_class: The node class to instantiate
142
+ node_id: The node identifier from workflow config
143
+ config: Node configuration parameters
144
+
145
+ Returns:
146
+ Instantiated node instance
147
+
148
+ Raises:
149
+ NodeConfigurationError: If node creation fails with detailed diagnostics
150
+ """
151
+ # Inspect the node constructor signature
152
+ sig = inspect.signature(node_class.__init__)
153
+ params = list(sig.parameters.keys())
154
+
155
+ try:
156
+ # Handle different constructor patterns
157
+ if "name" in params and "id" not in params:
158
+ # Node expects 'name' parameter (like PythonCodeNode)
159
+ if "name" not in config:
160
+ config = config.copy() # Don't modify original
161
+ config["name"] = node_id
162
+ return node_class(**config)
163
+ elif "id" in params:
164
+ # Node expects 'id' parameter (traditional pattern)
165
+ return node_class(id=node_id, **config)
166
+ else:
167
+ # Fallback: try both patterns
168
+ try:
169
+ return node_class(id=node_id, **config)
170
+ except TypeError:
171
+ # Try with name parameter
172
+ config = config.copy()
173
+ config["name"] = node_id
174
+ return node_class(**config)
175
+
176
+ except TypeError as e:
177
+ error_msg = str(e)
178
+ if "missing 1 required positional argument: 'name'" in error_msg:
179
+ raise NodeConfigurationError(
180
+ f"Node '{node_class.__name__}' requires 'name' parameter. "
181
+ f"Expected constructor signature includes 'name'. "
182
+ f"Config provided: {list(config.keys())}. "
183
+ f"Add 'name': '{node_id}' to node config."
184
+ ) from e
185
+ elif "unexpected keyword argument" in error_msg:
186
+ raise NodeConfigurationError(
187
+ f"Node '{node_class.__name__}' received unexpected parameters. "
188
+ f"Constructor signature: {sig}. "
189
+ f"Config provided: {list(config.keys())}."
190
+ ) from e
191
+ else:
192
+ raise NodeConfigurationError(
193
+ f"Failed to create node '{node_id}' of type '{node_class.__name__}': {e}. "
194
+ f"Constructor signature: {sig}. Config: {config}"
195
+ ) from e
196
+
131
197
  def add_node(self, node_id: str, node_or_type: Any, **config) -> None:
132
198
  """Add a node to the workflow.
133
199
 
@@ -151,11 +217,13 @@ class Workflow:
151
217
  if isinstance(node_or_type, str):
152
218
  # Node type name provided
153
219
  node_class = NodeRegistry.get(node_or_type)
154
- node_instance = node_class(id=node_id, **config)
220
+ node_instance = self._create_node_instance(node_class, node_id, config)
155
221
  node_type = node_or_type
156
222
  elif isinstance(node_or_type, type) and issubclass(node_or_type, Node):
157
223
  # Node class provided
158
- node_instance = node_or_type(id=node_id, **config)
224
+ node_instance = self._create_node_instance(
225
+ node_or_type, node_id, config
226
+ )
159
227
  node_type = node_or_type.__name__
160
228
  elif isinstance(node_or_type, Node):
161
229
  # Node instance provided