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.
- kailash/__init__.py +1 -7
- kailash/cli/__init__.py +11 -1
- kailash/cli/validation_audit.py +570 -0
- kailash/core/actors/supervisor.py +1 -1
- kailash/core/resilience/circuit_breaker.py +71 -1
- kailash/core/resilience/health_monitor.py +172 -0
- kailash/edge/compliance.py +33 -0
- kailash/edge/consistency.py +609 -0
- kailash/edge/coordination/__init__.py +30 -0
- kailash/edge/coordination/global_ordering.py +355 -0
- kailash/edge/coordination/leader_election.py +217 -0
- kailash/edge/coordination/partition_detector.py +296 -0
- kailash/edge/coordination/raft.py +485 -0
- kailash/edge/discovery.py +63 -1
- kailash/edge/migration/__init__.py +19 -0
- kailash/edge/migration/edge_migrator.py +832 -0
- kailash/edge/monitoring/__init__.py +21 -0
- kailash/edge/monitoring/edge_monitor.py +736 -0
- kailash/edge/prediction/__init__.py +10 -0
- kailash/edge/prediction/predictive_warmer.py +591 -0
- kailash/edge/resource/__init__.py +102 -0
- kailash/edge/resource/cloud_integration.py +796 -0
- kailash/edge/resource/cost_optimizer.py +949 -0
- kailash/edge/resource/docker_integration.py +919 -0
- kailash/edge/resource/kubernetes_integration.py +893 -0
- kailash/edge/resource/platform_integration.py +913 -0
- kailash/edge/resource/predictive_scaler.py +959 -0
- kailash/edge/resource/resource_analyzer.py +824 -0
- kailash/edge/resource/resource_pools.py +610 -0
- kailash/integrations/dataflow_edge.py +261 -0
- kailash/mcp_server/registry_integration.py +1 -1
- kailash/monitoring/__init__.py +18 -0
- kailash/monitoring/alerts.py +646 -0
- kailash/monitoring/metrics.py +677 -0
- kailash/nodes/__init__.py +2 -0
- kailash/nodes/ai/__init__.py +17 -0
- kailash/nodes/ai/a2a.py +1914 -43
- kailash/nodes/ai/a2a_backup.py +1807 -0
- kailash/nodes/ai/hybrid_search.py +972 -0
- kailash/nodes/ai/semantic_memory.py +558 -0
- kailash/nodes/ai/streaming_analytics.py +947 -0
- kailash/nodes/base.py +545 -0
- kailash/nodes/edge/__init__.py +36 -0
- kailash/nodes/edge/base.py +240 -0
- kailash/nodes/edge/cloud_node.py +710 -0
- kailash/nodes/edge/coordination.py +239 -0
- kailash/nodes/edge/docker_node.py +825 -0
- kailash/nodes/edge/edge_data.py +582 -0
- kailash/nodes/edge/edge_migration_node.py +392 -0
- kailash/nodes/edge/edge_monitoring_node.py +421 -0
- kailash/nodes/edge/edge_state.py +673 -0
- kailash/nodes/edge/edge_warming_node.py +393 -0
- kailash/nodes/edge/kubernetes_node.py +652 -0
- kailash/nodes/edge/platform_node.py +766 -0
- kailash/nodes/edge/resource_analyzer_node.py +378 -0
- kailash/nodes/edge/resource_optimizer_node.py +501 -0
- kailash/nodes/edge/resource_scaler_node.py +397 -0
- kailash/nodes/ports.py +676 -0
- kailash/runtime/local.py +344 -1
- kailash/runtime/validation/__init__.py +20 -0
- kailash/runtime/validation/connection_context.py +119 -0
- kailash/runtime/validation/enhanced_error_formatter.py +202 -0
- kailash/runtime/validation/error_categorizer.py +164 -0
- kailash/runtime/validation/metrics.py +380 -0
- kailash/runtime/validation/performance.py +615 -0
- kailash/runtime/validation/suggestion_engine.py +212 -0
- kailash/testing/fixtures.py +2 -2
- kailash/workflow/builder.py +234 -8
- kailash/workflow/contracts.py +418 -0
- kailash/workflow/edge_infrastructure.py +369 -0
- kailash/workflow/migration.py +3 -3
- kailash/workflow/type_inference.py +669 -0
- {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/METADATA +44 -27
- {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/RECORD +78 -28
- kailash/nexus/__init__.py +0 -21
- kailash/nexus/cli/__init__.py +0 -5
- kailash/nexus/cli/__main__.py +0 -6
- kailash/nexus/cli/main.py +0 -176
- kailash/nexus/factory.py +0 -413
- kailash/nexus/gateway.py +0 -545
- {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/WHEEL +0 -0
- {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/entry_points.txt +0 -0
- {kailash-0.8.3.dist-info → kailash-0.8.5.dist-info}/licenses/LICENSE +0 -0
- {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
|