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
@@ -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
|
kailash/workflow/__init__.py
CHANGED
@@ -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
|
-
#
|
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.
|
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
|
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 =
|
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
|