kailash 0.8.3__py3-none-any.whl → 0.8.5__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 (84) hide show
  1. kailash/__init__.py +1 -7
  2. kailash/cli/__init__.py +11 -1
  3. kailash/cli/validation_audit.py +570 -0
  4. kailash/core/actors/supervisor.py +1 -1
  5. kailash/core/resilience/circuit_breaker.py +71 -1
  6. kailash/core/resilience/health_monitor.py +172 -0
  7. kailash/edge/compliance.py +33 -0
  8. kailash/edge/consistency.py +609 -0
  9. kailash/edge/coordination/__init__.py +30 -0
  10. kailash/edge/coordination/global_ordering.py +355 -0
  11. kailash/edge/coordination/leader_election.py +217 -0
  12. kailash/edge/coordination/partition_detector.py +296 -0
  13. kailash/edge/coordination/raft.py +485 -0
  14. kailash/edge/discovery.py +63 -1
  15. kailash/edge/migration/__init__.py +19 -0
  16. kailash/edge/migration/edge_migrator.py +832 -0
  17. kailash/edge/monitoring/__init__.py +21 -0
  18. kailash/edge/monitoring/edge_monitor.py +736 -0
  19. kailash/edge/prediction/__init__.py +10 -0
  20. kailash/edge/prediction/predictive_warmer.py +591 -0
  21. kailash/edge/resource/__init__.py +102 -0
  22. kailash/edge/resource/cloud_integration.py +796 -0
  23. kailash/edge/resource/cost_optimizer.py +949 -0
  24. kailash/edge/resource/docker_integration.py +919 -0
  25. kailash/edge/resource/kubernetes_integration.py +893 -0
  26. kailash/edge/resource/platform_integration.py +913 -0
  27. kailash/edge/resource/predictive_scaler.py +959 -0
  28. kailash/edge/resource/resource_analyzer.py +824 -0
  29. kailash/edge/resource/resource_pools.py +610 -0
  30. kailash/integrations/dataflow_edge.py +261 -0
  31. kailash/mcp_server/registry_integration.py +1 -1
  32. kailash/monitoring/__init__.py +18 -0
  33. kailash/monitoring/alerts.py +646 -0
  34. kailash/monitoring/metrics.py +677 -0
  35. kailash/nodes/__init__.py +2 -0
  36. kailash/nodes/ai/__init__.py +17 -0
  37. kailash/nodes/ai/a2a.py +1914 -43
  38. kailash/nodes/ai/a2a_backup.py +1807 -0
  39. kailash/nodes/ai/hybrid_search.py +972 -0
  40. kailash/nodes/ai/semantic_memory.py +558 -0
  41. kailash/nodes/ai/streaming_analytics.py +947 -0
  42. kailash/nodes/base.py +545 -0
  43. kailash/nodes/edge/__init__.py +36 -0
  44. kailash/nodes/edge/base.py +240 -0
  45. kailash/nodes/edge/cloud_node.py +710 -0
  46. kailash/nodes/edge/coordination.py +239 -0
  47. kailash/nodes/edge/docker_node.py +825 -0
  48. kailash/nodes/edge/edge_data.py +582 -0
  49. kailash/nodes/edge/edge_migration_node.py +392 -0
  50. kailash/nodes/edge/edge_monitoring_node.py +421 -0
  51. kailash/nodes/edge/edge_state.py +673 -0
  52. kailash/nodes/edge/edge_warming_node.py +393 -0
  53. kailash/nodes/edge/kubernetes_node.py +652 -0
  54. kailash/nodes/edge/platform_node.py +766 -0
  55. kailash/nodes/edge/resource_analyzer_node.py +378 -0
  56. kailash/nodes/edge/resource_optimizer_node.py +501 -0
  57. kailash/nodes/edge/resource_scaler_node.py +397 -0
  58. kailash/nodes/ports.py +676 -0
  59. kailash/runtime/local.py +344 -1
  60. kailash/runtime/validation/__init__.py +20 -0
  61. kailash/runtime/validation/connection_context.py +119 -0
  62. kailash/runtime/validation/enhanced_error_formatter.py +202 -0
  63. kailash/runtime/validation/error_categorizer.py +164 -0
  64. kailash/runtime/validation/metrics.py +380 -0
  65. kailash/runtime/validation/performance.py +615 -0
  66. kailash/runtime/validation/suggestion_engine.py +212 -0
  67. kailash/testing/fixtures.py +2 -2
  68. kailash/workflow/builder.py +234 -8
  69. kailash/workflow/contracts.py +418 -0
  70. kailash/workflow/edge_infrastructure.py +369 -0
  71. kailash/workflow/migration.py +3 -3
  72. kailash/workflow/type_inference.py +669 -0
  73. {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/METADATA +44 -27
  74. {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/RECORD +78 -28
  75. kailash/nexus/__init__.py +0 -21
  76. kailash/nexus/cli/__init__.py +0 -5
  77. kailash/nexus/cli/__main__.py +0 -6
  78. kailash/nexus/cli/main.py +0 -176
  79. kailash/nexus/factory.py +0 -413
  80. kailash/nexus/gateway.py +0 -545
  81. {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/WHEEL +0 -0
  82. {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/entry_points.txt +0 -0
  83. {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/licenses/LICENSE +0 -0
  84. {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,669 @@
1
+ """
2
+ Connection Type Inference for Kailash Workflows.
3
+
4
+ This module provides automatic type checking and inference for workflow connections,
5
+ ensuring type safety while allowing reasonable type coercions. It integrates with
6
+ the port system and connection contracts to provide comprehensive validation.
7
+
8
+ Design Goals:
9
+ 1. Type Safety: Catch type mismatches early with clear error messages
10
+ 2. Flexibility: Allow reasonable type coercions (str->int, list->tuple, etc.)
11
+ 3. Performance: Fast inference with caching for repeated checks
12
+ 4. Integration: Work seamlessly with port system and contracts
13
+ 5. Developer Experience: Clear, actionable error messages
14
+
15
+ Example Usage:
16
+ from kailash.workflow.type_inference import TypeInferenceEngine
17
+
18
+ engine = TypeInferenceEngine()
19
+
20
+ # Check type compatibility
21
+ is_compatible = engine.check_compatibility(str, int) # False
22
+ is_compatible = engine.check_compatibility(str, Union[str, int]) # True
23
+
24
+ # Infer connection types
25
+ result = engine.infer_connection_type(source_port, target_port)
26
+ if not result.is_compatible:
27
+ print(f"Type error: {result.error_message}")
28
+ """
29
+
30
+ import inspect
31
+ import logging
32
+ from dataclasses import dataclass
33
+ from enum import Enum
34
+ from typing import (
35
+ Any,
36
+ Dict,
37
+ List,
38
+ Optional,
39
+ Set,
40
+ Tuple,
41
+ Type,
42
+ Union,
43
+ get_args,
44
+ get_origin,
45
+ get_type_hints,
46
+ )
47
+
48
+ from kailash.nodes.ports import InputPort, OutputPort, Port
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+
53
+ class CoercionRule(Enum):
54
+ """Type coercion rules for automatic type conversion."""
55
+
56
+ # Numeric coercions
57
+ INT_TO_FLOAT = "int_to_float"
58
+ FLOAT_TO_INT = "float_to_int" # May lose precision
59
+ STR_TO_INT = "str_to_int"
60
+ STR_TO_FLOAT = "str_to_float"
61
+ STR_TO_BOOL = "str_to_bool"
62
+ INT_TO_STR = "int_to_str"
63
+ FLOAT_TO_STR = "float_to_str"
64
+ BOOL_TO_STR = "bool_to_str"
65
+
66
+ # Collection coercions
67
+ LIST_TO_TUPLE = "list_to_tuple"
68
+ TUPLE_TO_LIST = "tuple_to_list"
69
+ STR_TO_LIST = "str_to_list" # Split string
70
+ LIST_TO_STR = "list_to_str" # Join list
71
+
72
+ # Dict coercions
73
+ DICT_TO_OBJECT = "dict_to_object" # For JSON-like data
74
+
75
+ # None handling
76
+ NONE_TO_OPTIONAL = "none_to_optional"
77
+
78
+
79
+ @dataclass
80
+ class TypeCompatibilityResult:
81
+ """Result of type compatibility checking."""
82
+
83
+ is_compatible: bool
84
+ confidence: float # 0.0 to 1.0
85
+ coercion_rule: Optional[CoercionRule] = None
86
+ error_message: Optional[str] = None
87
+ warning_message: Optional[str] = None
88
+
89
+ @property
90
+ def requires_coercion(self) -> bool:
91
+ """Check if this compatibility requires type coercion."""
92
+ return self.coercion_rule is not None
93
+
94
+ @property
95
+ def is_perfect_match(self) -> bool:
96
+ """Check if types match exactly without coercion."""
97
+ return self.is_compatible and not self.requires_coercion
98
+
99
+
100
+ @dataclass
101
+ class ConnectionInferenceResult:
102
+ """Result of connection type inference."""
103
+
104
+ source_type: Type
105
+ target_type: Type
106
+ compatibility: TypeCompatibilityResult
107
+ suggested_fixes: List[str]
108
+
109
+ @property
110
+ def is_compatible(self) -> bool:
111
+ """Check if connection is type-compatible."""
112
+ return self.compatibility.is_compatible
113
+
114
+ @property
115
+ def error_message(self) -> Optional[str]:
116
+ """Get error message if incompatible."""
117
+ return self.compatibility.error_message
118
+
119
+
120
+ class TypeInferenceEngine:
121
+ """Engine for automatic type inference and compatibility checking."""
122
+
123
+ def __init__(self):
124
+ """Initialize the type inference engine."""
125
+ self._compatibility_cache: Dict[Tuple[Type, Type], TypeCompatibilityResult] = {}
126
+ self._coercion_rules = self._build_coercion_rules()
127
+
128
+ def _build_coercion_rules(self) -> Dict[Tuple[Type, Type], CoercionRule]:
129
+ """Build the type coercion rules mapping."""
130
+ return {
131
+ # Numeric coercions
132
+ (int, float): CoercionRule.INT_TO_FLOAT,
133
+ (float, int): CoercionRule.FLOAT_TO_INT,
134
+ (str, int): CoercionRule.STR_TO_INT,
135
+ (str, float): CoercionRule.STR_TO_FLOAT,
136
+ (str, bool): CoercionRule.STR_TO_BOOL,
137
+ (int, str): CoercionRule.INT_TO_STR,
138
+ (float, str): CoercionRule.FLOAT_TO_STR,
139
+ (bool, str): CoercionRule.BOOL_TO_STR,
140
+ # Collection coercions
141
+ (list, tuple): CoercionRule.LIST_TO_TUPLE,
142
+ (tuple, list): CoercionRule.TUPLE_TO_LIST,
143
+ (str, list): CoercionRule.STR_TO_LIST,
144
+ (list, str): CoercionRule.LIST_TO_STR,
145
+ # Dict coercions
146
+ (dict, object): CoercionRule.DICT_TO_OBJECT,
147
+ }
148
+
149
+ def check_compatibility(
150
+ self, source_type: Type, target_type: Type, allow_coercion: bool = True
151
+ ) -> TypeCompatibilityResult:
152
+ """Check if source type is compatible with target type.
153
+
154
+ Args:
155
+ source_type: Source port type
156
+ target_type: Target port type
157
+ allow_coercion: Whether to allow type coercion
158
+
159
+ Returns:
160
+ TypeCompatibilityResult with compatibility details
161
+ """
162
+ # Check cache first
163
+ cache_key = (source_type, target_type, allow_coercion)
164
+ if cache_key in self._compatibility_cache:
165
+ return self._compatibility_cache[cache_key]
166
+
167
+ result = self._check_compatibility_impl(
168
+ source_type, target_type, allow_coercion
169
+ )
170
+
171
+ # Cache the result
172
+ self._compatibility_cache[cache_key] = result
173
+
174
+ return result
175
+
176
+ def _check_compatibility_impl(
177
+ self, source_type: Type, target_type: Type, allow_coercion: bool
178
+ ) -> TypeCompatibilityResult:
179
+ """Implementation of compatibility checking."""
180
+
181
+ # Handle None types
182
+ if source_type is type(None):
183
+ if self._is_optional_type(target_type):
184
+ return TypeCompatibilityResult(
185
+ is_compatible=True,
186
+ confidence=1.0,
187
+ coercion_rule=CoercionRule.NONE_TO_OPTIONAL,
188
+ )
189
+ else:
190
+ return TypeCompatibilityResult(
191
+ is_compatible=False,
192
+ confidence=0.0,
193
+ error_message=f"Cannot assign None to non-optional type {self._get_type_name(target_type)}",
194
+ )
195
+
196
+ # Handle Any types
197
+ if source_type is Any or target_type is Any:
198
+ return TypeCompatibilityResult(
199
+ is_compatible=True,
200
+ confidence=0.5, # Lower confidence for Any
201
+ warning_message="Using Any type reduces type safety",
202
+ )
203
+
204
+ # Exact match
205
+ if source_type == target_type:
206
+ return TypeCompatibilityResult(is_compatible=True, confidence=1.0)
207
+
208
+ # Check if source is subclass of target
209
+ if self._is_subclass_safe(source_type, target_type):
210
+ return TypeCompatibilityResult(is_compatible=True, confidence=0.9)
211
+
212
+ # Handle Union types
213
+ if self._is_union_type(target_type):
214
+ return self._check_union_compatibility(source_type, target_type)
215
+
216
+ if self._is_union_type(source_type):
217
+ return self._check_source_union_compatibility(source_type, target_type)
218
+
219
+ # Handle Optional types (Union[T, None])
220
+ if self._is_optional_type(target_type):
221
+ inner_type = self._get_optional_inner_type(target_type)
222
+ return self.check_compatibility(source_type, inner_type, allow_coercion)
223
+
224
+ # Handle generic types (List[T], Dict[K, V], etc.)
225
+ source_origin = get_origin(source_type)
226
+ target_origin = get_origin(target_type)
227
+
228
+ if source_origin and target_origin:
229
+ return self._check_generic_compatibility(
230
+ source_type, target_type, allow_coercion
231
+ )
232
+
233
+ # Handle type coercion
234
+ if allow_coercion:
235
+ coercion_result = self._check_coercion_compatibility(
236
+ source_type, target_type
237
+ )
238
+ if coercion_result.is_compatible:
239
+ return coercion_result
240
+
241
+ # No compatibility found
242
+ if allow_coercion:
243
+ error_msg = f"Type {self._get_type_name(source_type)} is not compatible with {self._get_type_name(target_type)}"
244
+ else:
245
+ error_msg = f"Type coercion not allowed: {self._get_type_name(source_type)} -> {self._get_type_name(target_type)}"
246
+
247
+ return TypeCompatibilityResult(
248
+ is_compatible=False, confidence=0.0, error_message=error_msg
249
+ )
250
+
251
+ def _check_union_compatibility(
252
+ self, source_type: Type, target_union: Type
253
+ ) -> TypeCompatibilityResult:
254
+ """Check if source type matches any type in the union."""
255
+ union_args = get_args(target_union)
256
+
257
+ best_result = None
258
+ best_confidence = 0.0
259
+
260
+ for union_type in union_args:
261
+ result = self.check_compatibility(
262
+ source_type, union_type, allow_coercion=True
263
+ )
264
+ if result.is_compatible and result.confidence > best_confidence:
265
+ best_result = result
266
+ best_confidence = result.confidence
267
+
268
+ # Perfect match found
269
+ if result.is_perfect_match:
270
+ break
271
+
272
+ if best_result and best_result.is_compatible:
273
+ return best_result
274
+
275
+ union_types = ", ".join(self._get_type_name(t) for t in union_args)
276
+ return TypeCompatibilityResult(
277
+ is_compatible=False,
278
+ confidence=0.0,
279
+ error_message=f"Type {self._get_type_name(source_type)} does not match any type in Union[{union_types}]",
280
+ )
281
+
282
+ def _check_source_union_compatibility(
283
+ self, source_union: Type, target_type: Type
284
+ ) -> TypeCompatibilityResult:
285
+ """Check if all types in source union are compatible with target."""
286
+ union_args = get_args(source_union)
287
+
288
+ incompatible_types = []
289
+ min_confidence = 1.0
290
+ requires_coercion = False
291
+
292
+ for union_type in union_args:
293
+ result = self.check_compatibility(
294
+ union_type, target_type, allow_coercion=True
295
+ )
296
+ if not result.is_compatible:
297
+ incompatible_types.append(self._get_type_name(union_type))
298
+ else:
299
+ min_confidence = min(min_confidence, result.confidence)
300
+ if result.requires_coercion:
301
+ requires_coercion = True
302
+
303
+ if incompatible_types:
304
+ return TypeCompatibilityResult(
305
+ is_compatible=False,
306
+ confidence=0.0,
307
+ error_message=f"Types in source union not compatible with {self._get_type_name(target_type)}: {', '.join(incompatible_types)}",
308
+ )
309
+
310
+ return TypeCompatibilityResult(
311
+ is_compatible=True,
312
+ confidence=min_confidence,
313
+ warning_message=(
314
+ "Union source type may require runtime type checking"
315
+ if requires_coercion
316
+ else None
317
+ ),
318
+ )
319
+
320
+ def _check_generic_compatibility(
321
+ self, source_type: Type, target_type: Type, allow_coercion: bool
322
+ ) -> TypeCompatibilityResult:
323
+ """Check compatibility of generic types (List[T], Dict[K,V], etc.)."""
324
+ source_origin = get_origin(source_type)
325
+ target_origin = get_origin(target_type)
326
+ source_args = get_args(source_type)
327
+ target_args = get_args(target_type)
328
+
329
+ # Origins must be compatible
330
+ if source_origin != target_origin:
331
+ # Check for coercible origins (list <-> tuple)
332
+ coercion_rule = self._coercion_rules.get((source_origin, target_origin))
333
+ if not (allow_coercion and coercion_rule):
334
+ return TypeCompatibilityResult(
335
+ is_compatible=False,
336
+ confidence=0.0,
337
+ error_message=f"Generic type origins incompatible: {source_origin} vs {target_origin}",
338
+ )
339
+
340
+ # Check type arguments
341
+ if len(source_args) != len(target_args):
342
+ return TypeCompatibilityResult(
343
+ is_compatible=False,
344
+ confidence=0.0,
345
+ error_message=f"Generic type argument count mismatch: {len(source_args)} vs {len(target_args)}",
346
+ )
347
+
348
+ min_confidence = 1.0
349
+ requires_coercion = False
350
+
351
+ for source_arg, target_arg in zip(source_args, target_args):
352
+ arg_result = self.check_compatibility(
353
+ source_arg, target_arg, allow_coercion
354
+ )
355
+ if not arg_result.is_compatible:
356
+ return TypeCompatibilityResult(
357
+ is_compatible=False,
358
+ confidence=0.0,
359
+ error_message=f"Generic type argument incompatible: {self._get_type_name(source_arg)} vs {self._get_type_name(target_arg)}",
360
+ )
361
+
362
+ min_confidence = min(min_confidence, arg_result.confidence)
363
+ if arg_result.requires_coercion:
364
+ requires_coercion = True
365
+
366
+ # Determine final coercion rule
367
+ final_coercion = None
368
+ if source_origin != target_origin:
369
+ final_coercion = self._coercion_rules.get((source_origin, target_origin))
370
+ elif requires_coercion:
371
+ final_coercion = CoercionRule.DICT_TO_OBJECT # Generic placeholder
372
+
373
+ return TypeCompatibilityResult(
374
+ is_compatible=True, confidence=min_confidence, coercion_rule=final_coercion
375
+ )
376
+
377
+ def _check_coercion_compatibility(
378
+ self, source_type: Type, target_type: Type
379
+ ) -> TypeCompatibilityResult:
380
+ """Check if types are compatible through coercion."""
381
+
382
+ # Direct coercion rule
383
+ coercion_rule = self._coercion_rules.get((source_type, target_type))
384
+ if coercion_rule:
385
+ confidence = self._get_coercion_confidence(coercion_rule)
386
+ warning = self._get_coercion_warning(coercion_rule)
387
+
388
+ return TypeCompatibilityResult(
389
+ is_compatible=True,
390
+ confidence=confidence,
391
+ coercion_rule=coercion_rule,
392
+ warning_message=warning,
393
+ )
394
+
395
+ # Check if target accepts source through inheritance
396
+ if hasattr(target_type, "__origin__"):
397
+ # Handle generic coercions
398
+ return self._check_generic_coercion(source_type, target_type)
399
+
400
+ # Check if source is generic and target is base type that could work
401
+ source_origin = get_origin(source_type)
402
+ if source_origin and not hasattr(target_type, "__origin__"):
403
+ # e.g., List[str] -> tuple
404
+ base_coercion = self._coercion_rules.get((source_origin, target_type))
405
+ if base_coercion:
406
+ return TypeCompatibilityResult(
407
+ is_compatible=True,
408
+ confidence=0.7,
409
+ coercion_rule=base_coercion,
410
+ warning_message="Generic to base type coercion may lose type information",
411
+ )
412
+
413
+ return TypeCompatibilityResult(
414
+ is_compatible=False,
415
+ confidence=0.0,
416
+ error_message=f"No coercion rule available for {self._get_type_name(source_type)} -> {self._get_type_name(target_type)}",
417
+ )
418
+
419
+ def _check_generic_coercion(
420
+ self, source_type: Type, target_type: Type
421
+ ) -> TypeCompatibilityResult:
422
+ """Check coercion for generic types."""
423
+ target_origin = get_origin(target_type)
424
+
425
+ # list -> List[T] coercion
426
+ if source_type is list and target_origin is list:
427
+ return TypeCompatibilityResult(
428
+ is_compatible=True,
429
+ confidence=0.8,
430
+ coercion_rule=CoercionRule.LIST_TO_TUPLE, # Placeholder
431
+ warning_message="Generic type coercion may lose type information",
432
+ )
433
+
434
+ # dict -> Dict[K, V] coercion
435
+ if source_type is dict and target_origin is dict:
436
+ return TypeCompatibilityResult(
437
+ is_compatible=True,
438
+ confidence=0.8,
439
+ coercion_rule=CoercionRule.DICT_TO_OBJECT,
440
+ warning_message="Generic type coercion may lose type information",
441
+ )
442
+
443
+ return TypeCompatibilityResult(is_compatible=False, confidence=0.0)
444
+
445
+ def _get_coercion_confidence(self, rule: CoercionRule) -> float:
446
+ """Get confidence level for a coercion rule."""
447
+ confidence_map = {
448
+ # High confidence (lossless)
449
+ CoercionRule.INT_TO_FLOAT: 0.9,
450
+ CoercionRule.INT_TO_STR: 0.9,
451
+ CoercionRule.BOOL_TO_STR: 0.9,
452
+ CoercionRule.LIST_TO_TUPLE: 0.9,
453
+ CoercionRule.TUPLE_TO_LIST: 0.9,
454
+ CoercionRule.NONE_TO_OPTIONAL: 1.0,
455
+ # Medium confidence (may have issues)
456
+ CoercionRule.STR_TO_INT: 0.7,
457
+ CoercionRule.STR_TO_FLOAT: 0.7,
458
+ CoercionRule.STR_TO_BOOL: 0.6,
459
+ CoercionRule.STR_TO_LIST: 0.6,
460
+ CoercionRule.LIST_TO_STR: 0.7,
461
+ # Lower confidence (may lose data)
462
+ CoercionRule.FLOAT_TO_INT: 0.5,
463
+ CoercionRule.FLOAT_TO_STR: 0.8,
464
+ CoercionRule.DICT_TO_OBJECT: 0.6,
465
+ }
466
+
467
+ return confidence_map.get(rule, 0.5)
468
+
469
+ def _get_coercion_warning(self, rule: CoercionRule) -> Optional[str]:
470
+ """Get warning message for a coercion rule."""
471
+ warning_map = {
472
+ CoercionRule.FLOAT_TO_INT: "Converting float to int may lose precision",
473
+ CoercionRule.STR_TO_INT: "String to int conversion may fail at runtime",
474
+ CoercionRule.STR_TO_FLOAT: "String to float conversion may fail at runtime",
475
+ CoercionRule.STR_TO_BOOL: "String to bool conversion uses truthiness rules",
476
+ CoercionRule.DICT_TO_OBJECT: "Dict to object conversion may lose type safety",
477
+ }
478
+
479
+ return warning_map.get(rule)
480
+
481
+ def infer_connection_type(
482
+ self, source_port: Port, target_port: Port, allow_coercion: bool = True
483
+ ) -> ConnectionInferenceResult:
484
+ """Infer type compatibility for a connection between ports.
485
+
486
+ Args:
487
+ source_port: Source output port
488
+ target_port: Target input port
489
+ allow_coercion: Whether to allow type coercion
490
+
491
+ Returns:
492
+ ConnectionInferenceResult with detailed analysis
493
+ """
494
+ # Get port types
495
+ source_type = source_port.type_hint or Any
496
+ target_type = target_port.type_hint or Any
497
+
498
+ # Check compatibility
499
+ compatibility = self.check_compatibility(
500
+ source_type, target_type, allow_coercion
501
+ )
502
+
503
+ # Generate suggested fixes
504
+ suggested_fixes = []
505
+ if not compatibility.is_compatible:
506
+ suggested_fixes = self._generate_fix_suggestions(
507
+ source_type, target_type, source_port, target_port
508
+ )
509
+
510
+ return ConnectionInferenceResult(
511
+ source_type=source_type,
512
+ target_type=target_type,
513
+ compatibility=compatibility,
514
+ suggested_fixes=suggested_fixes,
515
+ )
516
+
517
+ def _generate_fix_suggestions(
518
+ self, source_type: Type, target_type: Type, source_port: Port, target_port: Port
519
+ ) -> List[str]:
520
+ """Generate suggestions for fixing type incompatibility."""
521
+ suggestions = []
522
+
523
+ # Check if coercion is available
524
+ coercion_rule = self._coercion_rules.get((source_type, target_type))
525
+ if coercion_rule:
526
+ suggestions.append(
527
+ f"Add type coercion: {source_type.__name__} -> {target_type.__name__}"
528
+ )
529
+
530
+ # Check if making target optional would help
531
+ if source_type is type(None) and not self._is_optional_type(target_type):
532
+ suggestions.append(
533
+ f"Make target port optional: Optional[{self._get_type_name(target_type)}]"
534
+ )
535
+
536
+ # Check if union type would help
537
+ if not self._is_union_type(target_type):
538
+ suggestions.append(
539
+ f"Change target to union: Union[{self._get_type_name(target_type)}, {self._get_type_name(source_type)}]"
540
+ )
541
+
542
+ # Check for common mistakes
543
+ if source_type is str and target_type in (int, float):
544
+ suggestions.append("Ensure source string contains valid numeric value")
545
+
546
+ if source_type is list and target_type is str:
547
+ suggestions.append("Add string join operation between nodes")
548
+
549
+ if source_type is dict and target_type is not dict:
550
+ suggestions.append(
551
+ "Extract specific value from dict or serialize to string"
552
+ )
553
+
554
+ # Generic suggestions
555
+ suggestions.append(
556
+ f"Change source port type to {self._get_type_name(target_type)}"
557
+ )
558
+ suggestions.append(
559
+ f"Change target port type to {self._get_type_name(source_type)}"
560
+ )
561
+ suggestions.append("Add intermediate transformation node")
562
+
563
+ return suggestions[:5] # Limit to 5 suggestions
564
+
565
+ def _is_union_type(self, type_hint: Type) -> bool:
566
+ """Check if type is a Union."""
567
+ return get_origin(type_hint) is Union
568
+
569
+ def _is_optional_type(self, type_hint: Type) -> bool:
570
+ """Check if type is Optional (Union[T, None])."""
571
+ if not self._is_union_type(type_hint):
572
+ return False
573
+
574
+ args = get_args(type_hint)
575
+ return len(args) == 2 and type(None) in args
576
+
577
+ def _get_optional_inner_type(self, optional_type: Type) -> Type:
578
+ """Get the inner type from Optional[T]."""
579
+ args = get_args(optional_type)
580
+ return next(arg for arg in args if arg is not type(None))
581
+
582
+ def _is_subclass_safe(self, source_type: Type, target_type: Type) -> bool:
583
+ """Safely check if source is subclass of target."""
584
+ try:
585
+ if not isinstance(source_type, type) or not isinstance(target_type, type):
586
+ return False
587
+ return issubclass(source_type, target_type)
588
+ except TypeError:
589
+ return False
590
+
591
+ def _get_type_name(self, type_hint: Type) -> str:
592
+ """Get human-readable name for a type."""
593
+ if hasattr(type_hint, "__name__"):
594
+ return type_hint.__name__
595
+
596
+ origin = get_origin(type_hint)
597
+ if origin:
598
+ args = get_args(type_hint)
599
+ if origin is Union:
600
+ arg_names = [self._get_type_name(arg) for arg in args]
601
+ return f"Union[{', '.join(arg_names)}]"
602
+ elif args:
603
+ arg_names = [self._get_type_name(arg) for arg in args]
604
+ return f"{origin.__name__}[{', '.join(arg_names)}]"
605
+ else:
606
+ return origin.__name__
607
+
608
+ return str(type_hint)
609
+
610
+ def clear_cache(self) -> None:
611
+ """Clear the compatibility cache."""
612
+ self._compatibility_cache.clear()
613
+
614
+ def get_cache_stats(self) -> Dict[str, int]:
615
+ """Get cache statistics."""
616
+ return {
617
+ "cache_size": len(self._compatibility_cache),
618
+ "cache_hits": getattr(self, "_cache_hits", 0),
619
+ "cache_misses": getattr(self, "_cache_misses", 0),
620
+ }
621
+
622
+
623
+ # Global instance for convenience
624
+ _default_engine = None
625
+
626
+
627
+ def get_type_inference_engine() -> TypeInferenceEngine:
628
+ """Get the default type inference engine instance."""
629
+ global _default_engine
630
+ if _default_engine is None:
631
+ _default_engine = TypeInferenceEngine()
632
+ return _default_engine
633
+
634
+
635
+ def check_connection_compatibility(
636
+ source_port: Port, target_port: Port
637
+ ) -> ConnectionInferenceResult:
638
+ """Convenience function to check connection compatibility.
639
+
640
+ Args:
641
+ source_port: Source output port
642
+ target_port: Target input port
643
+
644
+ Returns:
645
+ ConnectionInferenceResult with compatibility analysis
646
+ """
647
+ engine = get_type_inference_engine()
648
+ return engine.infer_connection_type(source_port, target_port)
649
+
650
+
651
+ def validate_workflow_connections(
652
+ connections: List[Tuple[Port, Port]],
653
+ ) -> List[ConnectionInferenceResult]:
654
+ """Validate all connections in a workflow.
655
+
656
+ Args:
657
+ connections: List of (source_port, target_port) tuples
658
+
659
+ Returns:
660
+ List of ConnectionInferenceResult for each connection
661
+ """
662
+ engine = get_type_inference_engine()
663
+ results = []
664
+
665
+ for source_port, target_port in connections:
666
+ result = engine.infer_connection_type(source_port, target_port)
667
+ results.append(result)
668
+
669
+ return results