kailash 0.9.2__py3-none-any.whl → 0.9.3__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/runtime/local.py CHANGED
@@ -35,14 +35,20 @@ Examples:
35
35
  """
36
36
 
37
37
  import asyncio
38
+ import hashlib
38
39
  import logging
40
+ import time
41
+ from collections import defaultdict
39
42
  from datetime import UTC, datetime
40
- from typing import Any, Dict, Optional
43
+ from typing import Any, Dict, List, Optional, Set, Tuple, Union
41
44
 
42
45
  import networkx as nx
46
+ import psutil
43
47
 
44
48
  from kailash.nodes import Node
49
+ from kailash.runtime.compatibility_reporter import CompatibilityReporter
45
50
  from kailash.runtime.parameter_injector import WorkflowParameterInjector
51
+ from kailash.runtime.performance_monitor import ExecutionMetrics, PerformanceMonitor
46
52
  from kailash.runtime.secret_provider import EnvironmentSecretProvider, SecretProvider
47
53
  from kailash.runtime.validation.connection_context import ConnectionContext
48
54
  from kailash.runtime.validation.enhanced_error_formatter import EnhancedErrorFormatter
@@ -66,6 +72,32 @@ from kailash.workflow.cyclic_runner import CyclicWorkflowExecutor
66
72
 
67
73
  logger = logging.getLogger(__name__)
68
74
 
75
+ # Conditional execution imports (lazy-loaded to avoid circular imports)
76
+ _ConditionalBranchAnalyzer = None
77
+ _DynamicExecutionPlanner = None
78
+
79
+
80
+ def _get_conditional_analyzer():
81
+ """Lazy import ConditionalBranchAnalyzer to avoid circular imports."""
82
+ global _ConditionalBranchAnalyzer
83
+ if _ConditionalBranchAnalyzer is None:
84
+ from kailash.analysis.conditional_branch_analyzer import (
85
+ ConditionalBranchAnalyzer,
86
+ )
87
+
88
+ _ConditionalBranchAnalyzer = ConditionalBranchAnalyzer
89
+ return _ConditionalBranchAnalyzer
90
+
91
+
92
+ def _get_execution_planner():
93
+ """Lazy import DynamicExecutionPlanner to avoid circular imports."""
94
+ global _DynamicExecutionPlanner
95
+ if _DynamicExecutionPlanner is None:
96
+ from kailash.planning.dynamic_execution_planner import DynamicExecutionPlanner
97
+
98
+ _DynamicExecutionPlanner = DynamicExecutionPlanner
99
+ return _DynamicExecutionPlanner
100
+
69
101
 
70
102
  class LocalRuntime:
71
103
  """Unified runtime with enterprise capabilities.
@@ -96,6 +128,7 @@ class LocalRuntime:
96
128
  resource_limits: Optional[dict[str, Any]] = None,
97
129
  secret_provider: Optional[Any] = None,
98
130
  connection_validation: str = "warn",
131
+ conditional_execution: str = "route_data",
99
132
  ):
100
133
  """Initialize the unified runtime.
101
134
 
@@ -114,13 +147,24 @@ class LocalRuntime:
114
147
  - "off": No validation (backward compatibility)
115
148
  - "warn": Log warnings on validation errors (default)
116
149
  - "strict": Raise errors on validation failures
150
+ conditional_execution: Execution strategy for conditional routing:
151
+ - "route_data": Current behavior - all nodes execute, data routing only (default)
152
+ - "skip_branches": New behavior - skip unreachable branches entirely
117
153
  """
118
154
  # Validate connection_validation parameter
119
- valid_modes = {"off", "warn", "strict"}
120
- if connection_validation not in valid_modes:
155
+ valid_conn_modes = {"off", "warn", "strict"}
156
+ if connection_validation not in valid_conn_modes:
121
157
  raise ValueError(
122
158
  f"Invalid connection_validation mode: {connection_validation}. "
123
- f"Must be one of: {valid_modes}"
159
+ f"Must be one of: {valid_conn_modes}"
160
+ )
161
+
162
+ # Validate conditional_execution parameter
163
+ valid_exec_modes = {"route_data", "skip_branches"}
164
+ if conditional_execution not in valid_exec_modes:
165
+ raise ValueError(
166
+ f"Invalid conditional_execution mode: {conditional_execution}. "
167
+ f"Must be one of: {valid_exec_modes}"
124
168
  )
125
169
 
126
170
  self.debug = debug
@@ -134,6 +178,7 @@ class LocalRuntime:
134
178
  self.enable_audit = enable_audit
135
179
  self.resource_limits = resource_limits or {}
136
180
  self.connection_validation = connection_validation
181
+ self.conditional_execution = conditional_execution
137
182
  self.logger = logger
138
183
 
139
184
  # Enterprise feature managers (lazy initialization)
@@ -143,6 +188,30 @@ class LocalRuntime:
143
188
  if enable_cycles:
144
189
  self.cyclic_executor = CyclicWorkflowExecutor()
145
190
 
191
+ # Initialize conditional execution components (lazy initialization)
192
+ self._conditional_branch_analyzer = None
193
+ self._dynamic_execution_planner = None
194
+
195
+ # Phase 3: Basic Integration features
196
+ self._performance_monitor = None
197
+ self._compatibility_reporter = None
198
+ self._enable_performance_monitoring = False
199
+ self._performance_switch_enabled = False
200
+ self._enable_compatibility_reporting = False
201
+
202
+ # Phase 5: Production readiness features
203
+ self._execution_plan_cache = {}
204
+ self._performance_metrics = {}
205
+ self._fallback_metrics = {}
206
+ self._analytics_data = {
207
+ "conditional_executions": [],
208
+ "performance_history": [],
209
+ "cache_hits": 0,
210
+ "cache_misses": 0,
211
+ "execution_patterns": {},
212
+ "optimization_stats": {},
213
+ }
214
+
146
215
  # Configure logging
147
216
  if debug:
148
217
  self.logger.setLevel(logging.DEBUG)
@@ -405,10 +474,112 @@ class LocalRuntime:
405
474
  raise RuntimeExecutionError(
406
475
  f"Cyclic workflow execution failed: {e}"
407
476
  ) from e
477
+ elif (
478
+ self.conditional_execution == "skip_branches"
479
+ and self._has_conditional_patterns(workflow)
480
+ ):
481
+ # Check for automatic mode switching based on performance
482
+ current_mode = self.conditional_execution
483
+ if (
484
+ self._enable_performance_monitoring
485
+ and self._performance_switch_enabled
486
+ ):
487
+ should_switch, recommended_mode, reason = (
488
+ self._check_performance_switch(current_mode)
489
+ )
490
+ if should_switch:
491
+ self.logger.info(f"Switching execution mode: {reason}")
492
+ self.conditional_execution = recommended_mode
493
+ # If switching to route_data, use standard execution
494
+ if recommended_mode == "route_data":
495
+ results = await self._execute_workflow_async(
496
+ workflow=workflow,
497
+ task_manager=task_manager,
498
+ run_id=run_id,
499
+ parameters=processed_parameters or {},
500
+ workflow_context=workflow_context,
501
+ )
502
+ else:
503
+ # Continue with conditional execution
504
+ try:
505
+ results = await self._execute_conditional_approach(
506
+ workflow=workflow,
507
+ parameters=processed_parameters or {},
508
+ task_manager=task_manager,
509
+ run_id=run_id,
510
+ workflow_context=workflow_context,
511
+ )
512
+ except Exception as e:
513
+ self.logger.warning(
514
+ f"Conditional execution failed, falling back to standard execution: {e}"
515
+ )
516
+ # Fallback to standard execution
517
+ results = await self._execute_workflow_async(
518
+ workflow=workflow,
519
+ task_manager=task_manager,
520
+ run_id=run_id,
521
+ parameters=processed_parameters or {},
522
+ workflow_context=workflow_context,
523
+ )
524
+ else:
525
+ # No switch recommended, continue with current mode
526
+ self.logger.info(
527
+ "Conditional workflow detected, using conditional execution optimization"
528
+ )
529
+ try:
530
+ results = await self._execute_conditional_approach(
531
+ workflow=workflow,
532
+ parameters=processed_parameters or {},
533
+ task_manager=task_manager,
534
+ run_id=run_id,
535
+ workflow_context=workflow_context,
536
+ )
537
+ except Exception as e:
538
+ self.logger.warning(
539
+ f"Conditional execution failed, falling back to standard execution: {e}"
540
+ )
541
+ # Fallback to standard execution
542
+ results = await self._execute_workflow_async(
543
+ workflow=workflow,
544
+ task_manager=task_manager,
545
+ run_id=run_id,
546
+ parameters=processed_parameters or {},
547
+ workflow_context=workflow_context,
548
+ )
549
+ else:
550
+ # Performance monitoring disabled
551
+ self.logger.info(
552
+ "Conditional workflow detected, using conditional execution optimization"
553
+ )
554
+ try:
555
+ results = await self._execute_conditional_approach(
556
+ workflow=workflow,
557
+ parameters=processed_parameters or {},
558
+ task_manager=task_manager,
559
+ run_id=run_id,
560
+ workflow_context=workflow_context,
561
+ )
562
+ except Exception as e:
563
+ self.logger.warning(
564
+ f"Conditional execution failed, falling back to standard execution: {e}"
565
+ )
566
+ # Fallback to standard execution
567
+ results = await self._execute_workflow_async(
568
+ workflow=workflow,
569
+ task_manager=task_manager,
570
+ run_id=run_id,
571
+ parameters=processed_parameters or {},
572
+ workflow_context=workflow_context,
573
+ )
408
574
  else:
409
575
  # Execute standard DAG workflow with enterprise features
576
+ execution_mode = (
577
+ "route_data"
578
+ if self.conditional_execution == "route_data"
579
+ else "standard"
580
+ )
410
581
  self.logger.info(
411
- "Standard DAG workflow detected, using unified enterprise execution"
582
+ f"Standard DAG workflow detected, using unified enterprise execution ({execution_mode} mode)"
412
583
  )
413
584
  results = await self._execute_workflow_async(
414
585
  workflow=workflow,
@@ -545,6 +716,9 @@ class LocalRuntime:
545
716
  node_outputs = {}
546
717
  failed_nodes = []
547
718
 
719
+ # Make results available to _should_skip_conditional_node for transitive dependency checking
720
+ self._current_results = results
721
+
548
722
  # Use the workflow context passed from _execute_async
549
723
  if workflow_context is None:
550
724
  workflow_context = {}
@@ -638,6 +812,10 @@ class LocalRuntime:
638
812
 
639
813
  # CONDITIONAL EXECUTION: Skip nodes that only receive None inputs from conditional routing
640
814
  if self._should_skip_conditional_node(workflow, node_id, inputs):
815
+ if self.debug:
816
+ self.logger.debug(
817
+ f"DEBUG: Skipping {node_id} - inputs: {inputs}"
818
+ )
641
819
  self.logger.info(
642
820
  f"Skipping node {node_id} - all conditional inputs are None"
643
821
  )
@@ -886,19 +1064,44 @@ class LocalRuntime:
886
1064
  break
887
1065
 
888
1066
  if found:
889
- inputs[target_key] = value
890
- if self.debug:
891
- self.logger.debug(
892
- f" MAPPED: {source_key} -> {target_key} (type: {type(value)})"
893
- )
1067
+ # CONDITIONAL EXECUTION FIX: Don't overwrite existing non-None values with None
1068
+ # This handles cases where multiple edges map to the same input parameter
1069
+ if (
1070
+ target_key in inputs
1071
+ and inputs[target_key] is not None
1072
+ and value is None
1073
+ ):
1074
+ if self.debug:
1075
+ self.logger.debug(
1076
+ f" SKIP: Not overwriting existing non-None value for {target_key} with None from {source_node_id}"
1077
+ )
1078
+ else:
1079
+ inputs[target_key] = value
1080
+ if self.debug:
1081
+ self.logger.debug(
1082
+ f" MAPPED: {source_key} -> {target_key} (type: {type(value)})"
1083
+ )
894
1084
  else:
895
1085
  # Simple key mapping
896
1086
  if source_key in source_outputs:
897
- inputs[target_key] = source_outputs[source_key]
898
- if self.debug:
899
- self.logger.debug(
900
- f" MAPPED: {source_key} -> {target_key} (type: {type(source_outputs[source_key])})"
901
- )
1087
+ value = source_outputs[source_key]
1088
+ # CONDITIONAL EXECUTION FIX: Don't overwrite existing non-None values with None
1089
+ # This handles cases where multiple edges map to the same input parameter
1090
+ if (
1091
+ target_key in inputs
1092
+ and inputs[target_key] is not None
1093
+ and value is None
1094
+ ):
1095
+ if self.debug:
1096
+ self.logger.debug(
1097
+ f" SKIP: Not overwriting existing non-None value for {target_key} with None from {source_node_id}"
1098
+ )
1099
+ else:
1100
+ inputs[target_key] = value
1101
+ if self.debug:
1102
+ self.logger.debug(
1103
+ f" MAPPED: {source_key} -> {target_key} (type: {type(value)})"
1104
+ )
902
1105
  else:
903
1106
  if self.debug:
904
1107
  self.logger.debug(
@@ -1169,18 +1372,39 @@ class LocalRuntime:
1169
1372
  if not incoming_edges:
1170
1373
  return False
1171
1374
 
1172
- # Check if any incoming edges are from conditional nodes
1375
+ # Check for conditional inputs and analyze the nature of the data
1173
1376
  has_conditional_inputs = False
1377
+ has_non_none_connected_input = False
1378
+
1174
1379
  for source_node_id, _, edge_data in incoming_edges:
1175
1380
  source_node = workflow._node_instances.get(source_node_id)
1381
+ mapping = edge_data.get("mapping", {})
1382
+
1383
+ # Check if this edge provides any non-None inputs
1384
+ for source_key, target_key in mapping.items():
1385
+ if target_key in inputs and inputs[target_key] is not None:
1386
+ has_non_none_connected_input = True
1387
+
1388
+ # Direct connection from SwitchNode
1176
1389
  if source_node and source_node.__class__.__name__ in ["SwitchNode"]:
1177
1390
  has_conditional_inputs = True
1178
- break
1391
+ # Transitive dependency: source node was skipped due to conditional routing
1392
+ elif (
1393
+ hasattr(self, "_current_results")
1394
+ and source_node_id in self._current_results
1395
+ ):
1396
+ if self._current_results[source_node_id] is None:
1397
+ has_conditional_inputs = True
1179
1398
 
1180
1399
  # If no conditional inputs, don't skip
1181
1400
  if not has_conditional_inputs:
1182
1401
  return False
1183
1402
 
1403
+ # If we have conditional inputs but also have non-None data, don't skip
1404
+ # This handles mixed scenarios where some inputs are skipped but others provide data
1405
+ if has_non_none_connected_input:
1406
+ return False
1407
+
1184
1408
  # Get the node instance to check for configuration parameters
1185
1409
  node_instance = workflow._node_instances.get(node_id)
1186
1410
  if not node_instance:
@@ -1213,17 +1437,41 @@ class LocalRuntime:
1213
1437
  # Check if all connected inputs are None
1214
1438
  # This is the main condition for conditional routing
1215
1439
  has_non_none_input = False
1216
- for _, _, edge_data in incoming_edges:
1440
+
1441
+ # Count total connected inputs and None inputs from conditional sources
1442
+ total_connected_inputs = 0
1443
+ none_conditional_inputs = 0
1444
+
1445
+ for source_node_id, _, edge_data in incoming_edges:
1217
1446
  mapping = edge_data.get("mapping", {})
1218
1447
  for source_key, target_key in mapping.items():
1219
- if target_key in inputs and inputs[target_key] is not None:
1220
- has_non_none_input = True
1221
- break
1222
- if has_non_none_input:
1223
- break
1448
+ if target_key in inputs:
1449
+ total_connected_inputs += 1
1450
+ if inputs[target_key] is not None:
1451
+ has_non_none_input = True
1452
+ else:
1453
+ # Check if this None input came from conditional routing
1454
+ source_node = workflow._node_instances.get(source_node_id)
1455
+ is_from_conditional = (
1456
+ source_node
1457
+ and source_node.__class__.__name__ in ["SwitchNode"]
1458
+ ) or (
1459
+ hasattr(self, "_current_results")
1460
+ and source_node_id in self._current_results
1461
+ and self._current_results[source_node_id] is None
1462
+ )
1463
+ if is_from_conditional:
1464
+ none_conditional_inputs += 1
1465
+
1466
+ # Skip the node only if ALL connected inputs are None AND from conditional routing
1467
+ # This means nodes with mixed inputs (some None from conditional, some real data) should still execute
1468
+ if (
1469
+ total_connected_inputs > 0
1470
+ and none_conditional_inputs == total_connected_inputs
1471
+ ):
1472
+ return True
1224
1473
 
1225
- # Skip the node if all connected inputs are None
1226
- return not has_non_none_input
1474
+ return False
1227
1475
 
1228
1476
  def _should_stop_on_error(self, workflow: Workflow, node_id: str) -> bool:
1229
1477
  """Determine if execution should stop when a node fails.
@@ -1236,7 +1484,11 @@ class LocalRuntime:
1236
1484
  Whether to stop execution.
1237
1485
  """
1238
1486
  # Check if any downstream nodes depend on this node
1239
- has_dependents = workflow.graph.out_degree(node_id) > 0
1487
+ try:
1488
+ has_dependents = workflow.graph.out_degree(node_id) > 0
1489
+ except (TypeError, KeyError):
1490
+ # Handle case where node doesn't exist or graph issues
1491
+ has_dependents = False
1240
1492
 
1241
1493
  # For now, stop if the failed node has dependents
1242
1494
  # Future: implement configurable error handling policies
@@ -1675,3 +1927,1512 @@ class LocalRuntime:
1675
1927
  )
1676
1928
 
1677
1929
  return violations
1930
+
1931
+ def _has_conditional_patterns(self, workflow: Workflow) -> bool:
1932
+ """
1933
+ Check if workflow has conditional patterns (SwitchNodes) and is suitable for conditional execution.
1934
+
1935
+ CRITICAL: Only enable conditional execution for DAG workflows.
1936
+ Cyclic workflows must use normal execution to preserve cycle safety mechanisms.
1937
+
1938
+ Args:
1939
+ workflow: Workflow to check
1940
+
1941
+ Returns:
1942
+ True if workflow contains SwitchNode instances AND is a DAG (no cycles)
1943
+ """
1944
+ try:
1945
+ if not hasattr(workflow, "graph") or workflow.graph is None:
1946
+ return False
1947
+
1948
+ # CRITICAL: Check for cycles first - conditional execution is only safe for DAGs
1949
+ if self._workflow_has_cycles(workflow):
1950
+ self.logger.info(
1951
+ "Cyclic workflow detected - using normal execution to preserve cycle safety mechanisms"
1952
+ )
1953
+ return False
1954
+
1955
+ # Import here to avoid circular dependencies
1956
+ from kailash.analysis import ConditionalBranchAnalyzer
1957
+
1958
+ analyzer = ConditionalBranchAnalyzer(workflow)
1959
+ switch_nodes = analyzer._find_switch_nodes()
1960
+
1961
+ has_switches = len(switch_nodes) > 0
1962
+
1963
+ if has_switches:
1964
+ self.logger.debug(
1965
+ f"Found {len(switch_nodes)} SwitchNodes in DAG workflow - eligible for conditional execution"
1966
+ )
1967
+ else:
1968
+ self.logger.debug("No SwitchNodes found - using normal execution")
1969
+
1970
+ return has_switches
1971
+
1972
+ except Exception as e:
1973
+ self.logger.warning(f"Error checking conditional patterns: {e}")
1974
+ return False
1975
+
1976
+ def _workflow_has_cycles(self, workflow: Workflow) -> bool:
1977
+ """
1978
+ Detect if workflow has cycles using multiple detection methods.
1979
+
1980
+ Args:
1981
+ workflow: Workflow to check
1982
+
1983
+ Returns:
1984
+ True if workflow contains any cycles
1985
+ """
1986
+ try:
1987
+ # Method 1: Check for explicitly marked cycle connections
1988
+ if hasattr(workflow, "has_cycles") and callable(workflow.has_cycles):
1989
+ if workflow.has_cycles():
1990
+ self.logger.debug("Detected cycles via workflow.has_cycles()")
1991
+ return True
1992
+
1993
+ # Method 2: Check for cycle edges in connections
1994
+ if hasattr(workflow, "connections"):
1995
+ for connection in workflow.connections:
1996
+ if hasattr(connection, "cycle") and connection.cycle:
1997
+ self.logger.debug("Detected cycle via connection.cycle flag")
1998
+ return True
1999
+
2000
+ # Method 3: NetworkX graph cycle detection
2001
+ if hasattr(workflow, "graph") and workflow.graph is not None:
2002
+ import networkx as nx
2003
+
2004
+ is_dag = nx.is_directed_acyclic_graph(workflow.graph)
2005
+ if not is_dag:
2006
+ self.logger.debug("Detected cycles via NetworkX graph analysis")
2007
+ return True
2008
+
2009
+ # Method 4: Check graph edges for cycle metadata
2010
+ if hasattr(workflow, "graph") and workflow.graph is not None:
2011
+ for u, v, edge_data in workflow.graph.edges(data=True):
2012
+ if edge_data.get("cycle", False):
2013
+ self.logger.debug("Detected cycle via edge metadata")
2014
+ return True
2015
+
2016
+ return False
2017
+
2018
+ except Exception as e:
2019
+ self.logger.warning(f"Error detecting cycles: {e}")
2020
+ # On error, assume cycles exist for safety
2021
+ return True
2022
+
2023
+ async def _execute_conditional_approach(
2024
+ self,
2025
+ workflow: Workflow,
2026
+ parameters: dict[str, Any],
2027
+ task_manager: TaskManager,
2028
+ run_id: str,
2029
+ workflow_context: dict[str, Any],
2030
+ ) -> dict[str, dict[str, Any]]:
2031
+ """
2032
+ Execute workflow using conditional approach with two-phase execution.
2033
+
2034
+ Phase 1: Execute SwitchNodes to determine branches
2035
+ Phase 2: Execute only reachable nodes based on switch results
2036
+
2037
+ Args:
2038
+ workflow: Workflow to execute
2039
+ parameters: Node-specific parameters
2040
+ task_manager: Task manager for execution
2041
+ run_id: Unique run identifier
2042
+ workflow_context: Workflow execution context
2043
+
2044
+ Returns:
2045
+ Dictionary mapping node_id -> execution results
2046
+ """
2047
+ self.logger.info("Starting conditional execution approach")
2048
+ results = {}
2049
+ fallback_reason = None
2050
+ start_time = time.time()
2051
+ total_nodes = len(workflow.graph.nodes())
2052
+
2053
+ try:
2054
+ # Enhanced pre-execution validation
2055
+ if not self._validate_conditional_execution_prerequisites(workflow):
2056
+ fallback_reason = "Prerequisites validation failed"
2057
+ raise ValueError(
2058
+ f"Conditional execution prerequisites not met: {fallback_reason}"
2059
+ )
2060
+
2061
+ # Phase 1: Execute SwitchNodes to determine conditional branches
2062
+ self.logger.info("Phase 1: Executing SwitchNodes")
2063
+ phase1_results = await self._execute_switch_nodes(
2064
+ workflow=workflow,
2065
+ parameters=parameters,
2066
+ task_manager=task_manager,
2067
+ run_id=run_id,
2068
+ workflow_context=workflow_context,
2069
+ )
2070
+
2071
+ # Extract just switch results for validation and planning
2072
+ from kailash.analysis import ConditionalBranchAnalyzer
2073
+
2074
+ analyzer = ConditionalBranchAnalyzer(workflow)
2075
+ switch_node_ids = analyzer._find_switch_nodes()
2076
+ switch_results = {
2077
+ node_id: phase1_results[node_id]
2078
+ for node_id in switch_node_ids
2079
+ if node_id in phase1_results
2080
+ }
2081
+
2082
+ # Validate switch results before proceeding
2083
+ if not self._validate_switch_results(switch_results):
2084
+ fallback_reason = "Invalid switch results detected"
2085
+ raise ValueError(f"Switch results validation failed: {fallback_reason}")
2086
+
2087
+ # Add all phase 1 results to overall results
2088
+ results.update(phase1_results)
2089
+
2090
+ # Phase 2: Create pruned execution plan and execute remaining nodes
2091
+ self.logger.info("Phase 2: Creating and executing pruned plan")
2092
+ remaining_results = await self._execute_pruned_plan(
2093
+ workflow=workflow,
2094
+ switch_results=switch_results,
2095
+ parameters=parameters,
2096
+ task_manager=task_manager,
2097
+ run_id=run_id,
2098
+ workflow_context=workflow_context,
2099
+ existing_results=results,
2100
+ )
2101
+
2102
+ # Merge remaining results
2103
+ results.update(remaining_results)
2104
+
2105
+ # Final validation of conditional execution results
2106
+ if not self._validate_conditional_execution_results(results, workflow):
2107
+ fallback_reason = "Results validation failed"
2108
+ raise ValueError(
2109
+ f"Conditional execution results invalid: {fallback_reason}"
2110
+ )
2111
+
2112
+ # Performance tracking
2113
+ self._track_conditional_execution_performance(results, workflow)
2114
+
2115
+ # Record execution metrics for performance monitoring
2116
+ execution_time = time.time() - start_time
2117
+ nodes_executed = len(results)
2118
+ nodes_skipped = total_nodes - nodes_executed
2119
+
2120
+ self._record_execution_metrics(
2121
+ workflow=workflow,
2122
+ execution_time=execution_time,
2123
+ node_count=nodes_executed,
2124
+ skipped_nodes=nodes_skipped,
2125
+ execution_mode="skip_branches",
2126
+ )
2127
+
2128
+ # Log performance improvement
2129
+ if nodes_skipped > 0:
2130
+ skip_percentage = (nodes_skipped / total_nodes) * 100
2131
+ self.logger.info(
2132
+ f"Conditional execution performance: {skip_percentage:.1f}% reduction in executed nodes "
2133
+ f"({nodes_skipped}/{total_nodes} skipped)"
2134
+ )
2135
+
2136
+ self.logger.info(
2137
+ f"Conditional execution completed successfully: {nodes_executed} nodes executed"
2138
+ )
2139
+ return results
2140
+
2141
+ except Exception as e:
2142
+ # Enhanced error logging with fallback reasoning
2143
+ self.logger.error(f"Error in conditional execution approach: {e}")
2144
+ if fallback_reason:
2145
+ self.logger.warning(f"Fallback reason: {fallback_reason}")
2146
+
2147
+ # Log performance impact before fallback
2148
+ self._log_conditional_execution_failure(e, workflow, len(results))
2149
+
2150
+ # Enhanced fallback with detailed logging
2151
+ self.logger.warning(
2152
+ "Falling back to normal execution approach due to conditional execution failure"
2153
+ )
2154
+
2155
+ try:
2156
+ # Execute fallback with additional monitoring
2157
+ fallback_results, _ = await self._execute_async(
2158
+ workflow=workflow,
2159
+ parameters=parameters,
2160
+ task_manager=task_manager,
2161
+ )
2162
+
2163
+ # Track fallback usage for monitoring
2164
+ self._track_fallback_usage(workflow, str(e), fallback_reason)
2165
+
2166
+ return fallback_results
2167
+
2168
+ except Exception as fallback_error:
2169
+ self.logger.error(f"Fallback execution also failed: {fallback_error}")
2170
+ # If both conditional and fallback fail, re-raise the original error
2171
+ raise e from fallback_error
2172
+
2173
+ async def _execute_switch_nodes(
2174
+ self,
2175
+ workflow: Workflow,
2176
+ parameters: dict[str, Any],
2177
+ task_manager: TaskManager,
2178
+ run_id: str,
2179
+ workflow_context: dict[str, Any],
2180
+ ) -> dict[str, dict[str, Any]]:
2181
+ """
2182
+ Execute SwitchNodes first to determine conditional branches.
2183
+
2184
+ Args:
2185
+ workflow: Workflow being executed
2186
+ parameters: Node-specific parameters
2187
+ task_manager: Task manager for execution
2188
+ run_id: Unique run identifier
2189
+ workflow_context: Workflow execution context
2190
+
2191
+ Returns:
2192
+ Dictionary mapping switch_node_id -> execution results
2193
+ """
2194
+ self.logger.info("Phase 1: Executing SwitchNodes and their dependencies")
2195
+ all_phase1_results = {} # Store ALL results from Phase 1, not just switches
2196
+
2197
+ try:
2198
+ # Import here to avoid circular dependencies
2199
+ from kailash.analysis import ConditionalBranchAnalyzer
2200
+
2201
+ # Check if we should use hierarchical switch execution
2202
+ analyzer = ConditionalBranchAnalyzer(workflow)
2203
+ switch_node_ids = analyzer._find_switch_nodes()
2204
+
2205
+ if switch_node_ids and self._should_use_hierarchical_execution(
2206
+ workflow, switch_node_ids
2207
+ ):
2208
+ # Use hierarchical switch executor for complex switch patterns
2209
+ self.logger.info(
2210
+ "Using hierarchical switch execution for optimized performance"
2211
+ )
2212
+ from kailash.runtime.hierarchical_switch_executor import (
2213
+ HierarchicalSwitchExecutor,
2214
+ )
2215
+
2216
+ executor = HierarchicalSwitchExecutor(workflow, debug=self.debug)
2217
+
2218
+ # Define node executor function
2219
+ async def node_executor(
2220
+ node_id,
2221
+ node_instance,
2222
+ all_results,
2223
+ parameters,
2224
+ task_manager,
2225
+ workflow,
2226
+ workflow_context,
2227
+ ):
2228
+ node_inputs = self._prepare_node_inputs(
2229
+ workflow=workflow,
2230
+ node_id=node_id,
2231
+ node_instance=node_instance,
2232
+ node_outputs=all_results,
2233
+ parameters=parameters,
2234
+ )
2235
+
2236
+ result = await self._execute_single_node(
2237
+ node_id=node_id,
2238
+ node_instance=node_instance,
2239
+ node_inputs=node_inputs,
2240
+ task_manager=task_manager,
2241
+ workflow=workflow,
2242
+ workflow_context=workflow_context,
2243
+ run_id=run_id,
2244
+ )
2245
+ return result
2246
+
2247
+ # Execute switches hierarchically
2248
+ all_results, switch_results = (
2249
+ await executor.execute_switches_hierarchically(
2250
+ parameters=parameters,
2251
+ task_manager=task_manager,
2252
+ run_id=run_id,
2253
+ workflow_context=workflow_context,
2254
+ node_executor=node_executor,
2255
+ )
2256
+ )
2257
+
2258
+ # Log execution summary
2259
+ if self.debug:
2260
+ summary = executor.get_execution_summary(switch_results)
2261
+ self.logger.debug(f"Hierarchical execution summary: {summary}")
2262
+
2263
+ return all_results
2264
+
2265
+ # Otherwise, use standard execution
2266
+ self.logger.info("Using standard switch execution")
2267
+
2268
+ if not switch_node_ids:
2269
+ self.logger.info("No SwitchNodes found in workflow")
2270
+ return all_phase1_results
2271
+
2272
+ # Get topological order for all nodes
2273
+ all_nodes_order = list(nx.topological_sort(workflow.graph))
2274
+
2275
+ # Find all nodes that switches depend on (need to execute these too)
2276
+ nodes_to_execute = set(switch_node_ids)
2277
+ for switch_id in switch_node_ids:
2278
+ # Get all predecessors (direct and indirect) of this switch
2279
+ predecessors = nx.ancestors(workflow.graph, switch_id)
2280
+ nodes_to_execute.update(predecessors)
2281
+
2282
+ # Execute nodes in topological order, but only those needed for switches
2283
+ execution_order = [
2284
+ node_id for node_id in all_nodes_order if node_id in nodes_to_execute
2285
+ ]
2286
+
2287
+ self.logger.info(
2288
+ f"Executing {len(execution_order)} nodes in Phase 1 (switches and their dependencies)"
2289
+ )
2290
+ self.logger.debug(f"Phase 1 execution order: {execution_order}")
2291
+
2292
+ # Execute all nodes needed for switches in dependency order
2293
+ for node_id in execution_order:
2294
+ try:
2295
+ # Get node instance
2296
+ node_data = workflow.graph.nodes[node_id]
2297
+ # Try both 'node' and 'instance' keys for compatibility
2298
+ node_instance = node_data.get("node") or node_data.get("instance")
2299
+
2300
+ if node_instance is None:
2301
+ self.logger.warning(f"No instance found for node {node_id}")
2302
+ continue
2303
+
2304
+ # Prepare inputs for the node
2305
+ node_inputs = self._prepare_node_inputs(
2306
+ workflow=workflow,
2307
+ node_id=node_id,
2308
+ node_instance=node_instance,
2309
+ node_outputs=all_phase1_results, # Use all results so far
2310
+ parameters=parameters,
2311
+ )
2312
+
2313
+ # CRITICAL FIX: During phase 1, ensure SwitchNodes don't get their 'value' parameter
2314
+ # mistakenly used as 'input_data' when the actual input is missing
2315
+ if not node_inputs or "input_data" not in node_inputs:
2316
+ # Get incoming edges to check if input_data is expected
2317
+ has_input_connection = False
2318
+ for edge in workflow.graph.in_edges(switch_id, data=True):
2319
+ mapping = edge[2].get("mapping", {})
2320
+ if "input_data" in mapping.values():
2321
+ has_input_connection = True
2322
+ break
2323
+
2324
+ if has_input_connection:
2325
+ # If input_data is expected from a connection but not available,
2326
+ # explicitly set it to None to prevent config fallback
2327
+ node_inputs["input_data"] = None
2328
+
2329
+ # Execute the switch
2330
+ self.logger.debug(f"Executing SwitchNode: {switch_id}")
2331
+ result = await self._execute_single_node(
2332
+ node_id=node_id,
2333
+ node_instance=node_instance,
2334
+ node_inputs=node_inputs,
2335
+ task_manager=task_manager,
2336
+ workflow=workflow,
2337
+ run_id=run_id,
2338
+ workflow_context=workflow_context,
2339
+ )
2340
+
2341
+ all_phase1_results[node_id] = result
2342
+ self.logger.debug(
2343
+ f"Node {node_id} completed with result keys: {list(result.keys()) if isinstance(result, dict) else type(result)}"
2344
+ )
2345
+
2346
+ except Exception as e:
2347
+ self.logger.error(f"Error executing node {node_id}: {e}")
2348
+ # Continue with other nodes
2349
+ all_phase1_results[node_id] = {
2350
+ "error": str(e),
2351
+ "error_type": type(e).__name__,
2352
+ "failed": True,
2353
+ }
2354
+
2355
+ # Extract just switch results to return
2356
+ switch_results = {
2357
+ node_id: all_phase1_results[node_id]
2358
+ for node_id in switch_node_ids
2359
+ if node_id in all_phase1_results
2360
+ }
2361
+
2362
+ self.logger.info(
2363
+ f"Phase 1 completed: {len(all_phase1_results)} nodes executed ({len(switch_results)} switches)"
2364
+ )
2365
+ return all_phase1_results # Return ALL results, not just switches
2366
+
2367
+ except Exception as e:
2368
+ self.logger.error(f"Error in switch execution phase: {e}")
2369
+ return all_phase1_results
2370
+
2371
+ async def _execute_pruned_plan(
2372
+ self,
2373
+ workflow: Workflow,
2374
+ switch_results: dict[str, dict[str, Any]],
2375
+ parameters: dict[str, Any],
2376
+ task_manager: TaskManager,
2377
+ run_id: str,
2378
+ workflow_context: dict[str, Any],
2379
+ existing_results: dict[str, dict[str, Any]],
2380
+ ) -> dict[str, dict[str, Any]]:
2381
+ """
2382
+ Execute pruned execution plan based on SwitchNode results.
2383
+
2384
+ Args:
2385
+ workflow: Workflow being executed
2386
+ switch_results: Results from SwitchNode execution
2387
+ parameters: Node-specific parameters
2388
+ task_manager: Task manager for execution
2389
+ run_id: Unique run identifier
2390
+ workflow_context: Workflow execution context
2391
+ existing_results: Results from previous execution phases
2392
+
2393
+ Returns:
2394
+ Dictionary mapping node_id -> execution results for remaining nodes
2395
+ """
2396
+ self.logger.info("Phase 2: Executing pruned plan based on switch results")
2397
+ remaining_results = {}
2398
+
2399
+ try:
2400
+ # Import here to avoid circular dependencies
2401
+ from kailash.planning import DynamicExecutionPlanner
2402
+
2403
+ planner = DynamicExecutionPlanner(workflow)
2404
+
2405
+ # Create execution plan based on switch results
2406
+ execution_plan = planner.create_execution_plan(switch_results)
2407
+ self.logger.debug(
2408
+ f"DynamicExecutionPlanner returned plan: {execution_plan}"
2409
+ )
2410
+
2411
+ # Remove nodes that were already executed, but check if switches need re-execution
2412
+ already_executed = set(existing_results.keys())
2413
+ self.logger.debug(
2414
+ f"Already executed nodes from Phase 1: {already_executed}"
2415
+ )
2416
+ self.logger.debug(f"Full execution plan for Phase 2: {execution_plan}")
2417
+
2418
+ # Check which switches had incomplete execution (no input_data in phase 1)
2419
+ switches_needing_reexecution = set()
2420
+ for switch_id, result in switch_results.items():
2421
+ # If a switch executed with None input in phase 1, it needs re-execution
2422
+ if (
2423
+ result.get("true_output") is None
2424
+ and result.get("false_output") is None
2425
+ and switch_id in execution_plan
2426
+ ):
2427
+ # Check if this switch has dependencies that will now provide data
2428
+ has_dependencies = False
2429
+ for edge in workflow.graph.in_edges(switch_id):
2430
+ source_node = edge[0]
2431
+ if source_node in execution_plan:
2432
+ has_dependencies = True
2433
+ break
2434
+
2435
+ if has_dependencies:
2436
+ switches_needing_reexecution.add(switch_id)
2437
+ self.logger.debug(
2438
+ f"Switch {switch_id} needs re-execution with actual data"
2439
+ )
2440
+
2441
+ # Include switches that need re-execution AND any nodes not yet executed
2442
+ remaining_nodes = [
2443
+ node_id
2444
+ for node_id in execution_plan
2445
+ if node_id not in already_executed
2446
+ or node_id in switches_needing_reexecution
2447
+ ]
2448
+
2449
+ # Debug log to understand what's happening
2450
+ not_executed = set(execution_plan) - already_executed
2451
+ self.logger.debug(
2452
+ f"Nodes in execution plan but not executed: {not_executed}"
2453
+ )
2454
+ self.logger.debug(
2455
+ f"Switches needing re-execution: {switches_needing_reexecution}"
2456
+ )
2457
+ self.logger.debug(f"Filtering logic: remaining_nodes = {remaining_nodes}")
2458
+
2459
+ self.logger.info(
2460
+ f"Executing {len(remaining_nodes)} remaining nodes after pruning"
2461
+ )
2462
+ self.logger.debug(f"Remaining execution plan: {remaining_nodes}")
2463
+
2464
+ # Execute remaining nodes in the pruned order
2465
+ for node_id in remaining_nodes:
2466
+ try:
2467
+ # Get node instance
2468
+ node_data = workflow.graph.nodes[node_id]
2469
+ # Try both 'node' and 'instance' keys for compatibility
2470
+ node_instance = node_data.get("node") or node_data.get("instance")
2471
+
2472
+ if node_instance is None:
2473
+ self.logger.warning(f"No instance found for node {node_id}")
2474
+ continue
2475
+
2476
+ # Prepare inputs using all results so far (switches + remaining)
2477
+ all_results = {**existing_results, **remaining_results}
2478
+ node_inputs = self._prepare_node_inputs(
2479
+ workflow=workflow,
2480
+ node_id=node_id,
2481
+ node_instance=node_instance,
2482
+ node_outputs=all_results,
2483
+ parameters=parameters,
2484
+ )
2485
+
2486
+ # Execute the node
2487
+ self.logger.debug(f"Executing remaining node: {node_id}")
2488
+ result = await self._execute_single_node(
2489
+ node_id=node_id,
2490
+ node_instance=node_instance,
2491
+ node_inputs=node_inputs,
2492
+ task_manager=task_manager,
2493
+ workflow=workflow,
2494
+ run_id=run_id,
2495
+ workflow_context=workflow_context,
2496
+ )
2497
+
2498
+ remaining_results[node_id] = result
2499
+ self.logger.debug(f"Node {node_id} completed")
2500
+
2501
+ except Exception as e:
2502
+ self.logger.error(f"Error executing remaining node {node_id}: {e}")
2503
+ # Continue with other nodes or stop based on error handling
2504
+ if self._should_stop_on_error(workflow, node_id):
2505
+ raise
2506
+ else:
2507
+ remaining_results[node_id] = {
2508
+ "error": str(e),
2509
+ "error_type": type(e).__name__,
2510
+ "failed": True,
2511
+ }
2512
+
2513
+ self.logger.info(
2514
+ f"Phase 2 completed: {len(remaining_results)} remaining nodes executed"
2515
+ )
2516
+ return remaining_results
2517
+
2518
+ except Exception as e:
2519
+ self.logger.error(f"Error in pruned plan execution: {e}")
2520
+ return remaining_results
2521
+
2522
+ async def _execute_single_node(
2523
+ self,
2524
+ node_id: str,
2525
+ node_instance: Any,
2526
+ node_inputs: dict[str, Any],
2527
+ task_manager: Any,
2528
+ workflow: Workflow,
2529
+ run_id: str,
2530
+ workflow_context: dict[str, Any],
2531
+ ) -> dict[str, Any]:
2532
+ """
2533
+ Execute a single node with proper validation and context setup.
2534
+
2535
+ Args:
2536
+ node_id: Node identifier
2537
+ node_instance: Node instance to execute
2538
+ node_inputs: Prepared inputs for the node
2539
+ task_manager: Task manager for tracking
2540
+ workflow: Workflow being executed
2541
+ run_id: Unique run identifier
2542
+ workflow_context: Workflow execution context
2543
+
2544
+ Returns:
2545
+ Node execution results
2546
+ """
2547
+ # Validate inputs before execution
2548
+ from kailash.utils.data_validation import DataTypeValidator
2549
+
2550
+ validated_inputs = DataTypeValidator.validate_node_input(node_id, node_inputs)
2551
+
2552
+ # Set workflow context on the node instance
2553
+ if hasattr(node_instance, "_workflow_context"):
2554
+ node_instance._workflow_context = workflow_context
2555
+ else:
2556
+ # Initialize the workflow context if it doesn't exist
2557
+ node_instance._workflow_context = workflow_context
2558
+
2559
+ # Execute the node with unified async/sync support
2560
+ if self.enable_async and hasattr(node_instance, "execute_async"):
2561
+ # Use async execution method that includes validation
2562
+ outputs = await node_instance.execute_async(**validated_inputs)
2563
+ else:
2564
+ # Standard synchronous execution
2565
+ outputs = node_instance.execute(**validated_inputs)
2566
+
2567
+ return outputs
2568
+
2569
+ def _should_use_hierarchical_execution(
2570
+ self, workflow: Workflow, switch_node_ids: List[str]
2571
+ ) -> bool:
2572
+ """
2573
+ Determine if hierarchical switch execution should be used.
2574
+
2575
+ Args:
2576
+ workflow: The workflow to analyze
2577
+ switch_node_ids: List of switch node IDs
2578
+
2579
+ Returns:
2580
+ True if hierarchical execution would be beneficial
2581
+ """
2582
+ # Use hierarchical execution if:
2583
+ # 1. There are multiple switches
2584
+ if len(switch_node_ids) < 2:
2585
+ return False
2586
+
2587
+ # 2. Check if switches have dependencies on each other
2588
+ from kailash.analysis import ConditionalBranchAnalyzer
2589
+
2590
+ analyzer = ConditionalBranchAnalyzer(workflow)
2591
+ hierarchy_info = analyzer.analyze_switch_hierarchies(switch_node_ids)
2592
+
2593
+ # Use hierarchical if there are multiple execution layers
2594
+ execution_layers = hierarchy_info.get("execution_layers", [])
2595
+ if len(execution_layers) > 1:
2596
+ self.logger.debug(
2597
+ f"Detected {len(execution_layers)} execution layers in switch hierarchy"
2598
+ )
2599
+ return True
2600
+
2601
+ # Use hierarchical if there are dependency chains
2602
+ dependency_chains = hierarchy_info.get("dependency_chains", [])
2603
+ if dependency_chains and any(len(chain) > 1 for chain in dependency_chains):
2604
+ self.logger.debug("Detected dependency chains in switch hierarchy")
2605
+ return True
2606
+
2607
+ return False
2608
+
2609
+ def _validate_conditional_execution_prerequisites(self, workflow: Workflow) -> bool:
2610
+ """
2611
+ Validate that workflow meets prerequisites for conditional execution.
2612
+
2613
+ Args:
2614
+ workflow: Workflow to validate
2615
+
2616
+ Returns:
2617
+ True if prerequisites are met, False otherwise
2618
+ """
2619
+ try:
2620
+ # Check if workflow has at least one SwitchNode
2621
+ from kailash.analysis import ConditionalBranchAnalyzer
2622
+
2623
+ analyzer = ConditionalBranchAnalyzer(workflow)
2624
+ switch_nodes = analyzer._find_switch_nodes()
2625
+
2626
+ if not switch_nodes:
2627
+ self.logger.debug(
2628
+ "No SwitchNodes found - cannot use conditional execution"
2629
+ )
2630
+ return False
2631
+
2632
+ # Check if workflow is too complex for conditional execution
2633
+ if len(workflow.graph.nodes) > 100: # Configurable threshold
2634
+ self.logger.warning(
2635
+ "Workflow too large for conditional execution optimization"
2636
+ )
2637
+ return False
2638
+
2639
+ # Validate that all SwitchNodes have proper outputs
2640
+ for switch_id in switch_nodes:
2641
+ node_data = workflow.graph.nodes[switch_id]
2642
+ node_instance = node_data.get("node") or node_data.get("instance")
2643
+
2644
+ if node_instance is None:
2645
+ self.logger.warning(f"SwitchNode {switch_id} has no instance")
2646
+ return False
2647
+
2648
+ # Check if the SwitchNode has proper output configuration
2649
+ # SwitchNode might store condition_field in different ways
2650
+ has_condition = (
2651
+ hasattr(node_instance, "condition_field")
2652
+ or hasattr(node_instance, "_condition_field")
2653
+ or (
2654
+ hasattr(node_instance, "parameters")
2655
+ and "condition_field"
2656
+ in getattr(node_instance, "parameters", {})
2657
+ )
2658
+ or "SwitchNode"
2659
+ in str(type(node_instance)) # Type-based validation as fallback
2660
+ )
2661
+
2662
+ if not has_condition:
2663
+ self.logger.debug(
2664
+ f"SwitchNode {switch_id} condition validation unclear - allowing execution"
2665
+ )
2666
+ # Don't fail here - let conditional execution attempt and fall back if needed
2667
+
2668
+ return True
2669
+
2670
+ except Exception as e:
2671
+ self.logger.warning(
2672
+ f"Error validating conditional execution prerequisites: {e}"
2673
+ )
2674
+ return False
2675
+
2676
+ def _validate_switch_results(
2677
+ self, switch_results: dict[str, dict[str, Any]]
2678
+ ) -> bool:
2679
+ """
2680
+ Validate that switch results are valid for conditional execution.
2681
+
2682
+ Args:
2683
+ switch_results: Results from SwitchNode execution
2684
+
2685
+ Returns:
2686
+ True if results are valid, False otherwise
2687
+ """
2688
+ try:
2689
+ if not switch_results:
2690
+ self.logger.debug("No switch results to validate")
2691
+ return True
2692
+
2693
+ for switch_id, result in switch_results.items():
2694
+ # Check for execution errors
2695
+ if isinstance(result, dict) and result.get("failed"):
2696
+ self.logger.warning(
2697
+ f"SwitchNode {switch_id} failed during execution"
2698
+ )
2699
+ return False
2700
+
2701
+ # Validate result structure
2702
+ if not isinstance(result, dict):
2703
+ self.logger.warning(
2704
+ f"SwitchNode {switch_id} returned invalid result type: {type(result)}"
2705
+ )
2706
+ return False
2707
+
2708
+ # Check for required output keys (at least one branch should be present)
2709
+ has_output = any(
2710
+ key in result for key in ["true_output", "false_output"]
2711
+ )
2712
+ if not has_output:
2713
+ self.logger.warning(
2714
+ f"SwitchNode {switch_id} missing required output keys"
2715
+ )
2716
+ return False
2717
+
2718
+ return True
2719
+
2720
+ except Exception as e:
2721
+ self.logger.warning(f"Error validating switch results: {e}")
2722
+ return False
2723
+
2724
+ def _validate_conditional_execution_results(
2725
+ self, results: dict[str, dict[str, Any]], workflow: Workflow
2726
+ ) -> bool:
2727
+ """
2728
+ Validate final results from conditional execution.
2729
+
2730
+ Args:
2731
+ results: Execution results
2732
+ workflow: Original workflow
2733
+
2734
+ Returns:
2735
+ True if results are valid, False otherwise
2736
+ """
2737
+ try:
2738
+ # Check that at least some nodes executed
2739
+ if not results:
2740
+ self.logger.warning("No results from conditional execution")
2741
+ return False
2742
+
2743
+ # Validate that critical nodes (if any) were executed
2744
+ # This could be expanded based on workflow metadata
2745
+ total_nodes = len(workflow.graph.nodes)
2746
+ executed_nodes = len(results)
2747
+
2748
+ # If we executed less than 30% of nodes, might be an issue
2749
+ if executed_nodes < (total_nodes * 0.3):
2750
+ self.logger.warning(
2751
+ f"Conditional execution only ran {executed_nodes}/{total_nodes} nodes - might indicate an issue"
2752
+ )
2753
+ # Don't fail here, but log for monitoring
2754
+
2755
+ # Check for excessive failures
2756
+ failed_nodes = sum(
2757
+ 1
2758
+ for result in results.values()
2759
+ if isinstance(result, dict) and result.get("failed")
2760
+ )
2761
+
2762
+ if failed_nodes > (executed_nodes * 0.5):
2763
+ self.logger.warning(
2764
+ f"Too many node failures: {failed_nodes}/{executed_nodes}"
2765
+ )
2766
+ return False
2767
+
2768
+ return True
2769
+
2770
+ except Exception as e:
2771
+ self.logger.warning(f"Error validating conditional execution results: {e}")
2772
+ return False
2773
+
2774
+ def _track_conditional_execution_performance(
2775
+ self, results: dict[str, dict[str, Any]], workflow: Workflow
2776
+ ):
2777
+ """
2778
+ Track performance metrics for conditional execution.
2779
+
2780
+ Args:
2781
+ results: Execution results
2782
+ workflow: Original workflow
2783
+ """
2784
+ try:
2785
+ total_nodes = len(workflow.graph.nodes)
2786
+ executed_nodes = len(results)
2787
+ skipped_nodes = total_nodes - executed_nodes
2788
+
2789
+ # Log performance metrics
2790
+ if skipped_nodes > 0:
2791
+ performance_improvement = (skipped_nodes / total_nodes) * 100
2792
+ self.logger.info(
2793
+ f"Conditional execution performance: {performance_improvement:.1f}% reduction in executed nodes ({skipped_nodes}/{total_nodes} skipped)"
2794
+ )
2795
+
2796
+ # Track for monitoring (could be sent to metrics system)
2797
+ if hasattr(self, "_performance_metrics"):
2798
+ self._performance_metrics["conditional_execution"] = {
2799
+ "total_nodes": total_nodes,
2800
+ "executed_nodes": executed_nodes,
2801
+ "skipped_nodes": skipped_nodes,
2802
+ "performance_improvement_percent": (
2803
+ (skipped_nodes / total_nodes) * 100 if total_nodes > 0 else 0
2804
+ ),
2805
+ }
2806
+
2807
+ except Exception as e:
2808
+ self.logger.warning(
2809
+ f"Error tracking conditional execution performance: {e}"
2810
+ )
2811
+
2812
+ def _log_conditional_execution_failure(
2813
+ self, error: Exception, workflow: Workflow, nodes_completed: int
2814
+ ):
2815
+ """
2816
+ Log detailed information about conditional execution failure.
2817
+
2818
+ Args:
2819
+ error: Exception that caused the failure
2820
+ workflow: Workflow that failed
2821
+ nodes_completed: Number of nodes that completed before failure
2822
+ """
2823
+ try:
2824
+ total_nodes = len(workflow.graph.nodes)
2825
+
2826
+ self.logger.error(
2827
+ f"Conditional execution failed after {nodes_completed}/{total_nodes} nodes"
2828
+ )
2829
+ self.logger.error(f"Error type: {type(error).__name__}")
2830
+ self.logger.error(f"Error message: {str(error)}")
2831
+
2832
+ # Log workflow characteristics for debugging
2833
+ from kailash.analysis import ConditionalBranchAnalyzer
2834
+
2835
+ analyzer = ConditionalBranchAnalyzer(workflow)
2836
+ switch_nodes = analyzer._find_switch_nodes()
2837
+
2838
+ self.logger.debug(
2839
+ f"Workflow characteristics: {len(switch_nodes)} switches, {total_nodes} total nodes"
2840
+ )
2841
+
2842
+ except Exception as log_error:
2843
+ self.logger.warning(
2844
+ f"Error logging conditional execution failure: {log_error}"
2845
+ )
2846
+
2847
+ def _track_fallback_usage(
2848
+ self, workflow: Workflow, error_message: str, fallback_reason: str
2849
+ ):
2850
+ """
2851
+ Track fallback usage for monitoring and optimization.
2852
+
2853
+ Args:
2854
+ workflow: Workflow that required fallback
2855
+ error_message: Error that triggered fallback
2856
+ fallback_reason: Reason for fallback
2857
+ """
2858
+ try:
2859
+ import time
2860
+
2861
+ # Log fallback usage
2862
+ self.logger.info(
2863
+ f"Fallback used for workflow '{workflow.name}': {fallback_reason}"
2864
+ )
2865
+
2866
+ # Track for monitoring (could be sent to metrics system)
2867
+ if hasattr(self, "_fallback_metrics"):
2868
+ if "fallback_usage" not in self._fallback_metrics:
2869
+ self._fallback_metrics["fallback_usage"] = []
2870
+
2871
+ self._fallback_metrics["fallback_usage"].append(
2872
+ {
2873
+ "workflow_name": workflow.name,
2874
+ "workflow_id": workflow.workflow_id,
2875
+ "error_message": error_message,
2876
+ "fallback_reason": fallback_reason,
2877
+ "timestamp": time.time(),
2878
+ }
2879
+ )
2880
+
2881
+ # Limit tracking history to prevent memory growth
2882
+ if (
2883
+ hasattr(self, "_fallback_metrics")
2884
+ and len(self._fallback_metrics.get("fallback_usage", [])) > 100
2885
+ ):
2886
+ self._fallback_metrics["fallback_usage"] = self._fallback_metrics[
2887
+ "fallback_usage"
2888
+ ][-50:]
2889
+
2890
+ except Exception as e:
2891
+ self.logger.warning(f"Error tracking fallback usage: {e}")
2892
+
2893
+ # ===== PHASE 5: PRODUCTION READINESS =====
2894
+
2895
+ def get_execution_plan_cached(
2896
+ self, workflow: Workflow, switch_results: Dict[str, Dict[str, Any]]
2897
+ ) -> List[str]:
2898
+ """
2899
+ Get execution plan with caching for improved performance.
2900
+
2901
+ Args:
2902
+ workflow: Workflow to create execution plan for
2903
+ switch_results: Results from SwitchNode execution
2904
+
2905
+ Returns:
2906
+ Cached or newly computed execution plan
2907
+ """
2908
+ # Create cache key based on workflow structure and switch results
2909
+ cache_key = self._create_execution_plan_cache_key(workflow, switch_results)
2910
+
2911
+ if cache_key in self._execution_plan_cache:
2912
+ self._analytics_data["cache_hits"] += 1
2913
+ self.logger.debug(f"Cache hit for execution plan: {cache_key[:32]}...")
2914
+ return self._execution_plan_cache[cache_key]
2915
+
2916
+ # Cache miss - compute new plan
2917
+ self._analytics_data["cache_misses"] += 1
2918
+ self.logger.debug(f"Cache miss for execution plan: {cache_key[:32]}...")
2919
+
2920
+ try:
2921
+ from kailash.planning import DynamicExecutionPlanner
2922
+
2923
+ planner = DynamicExecutionPlanner(workflow)
2924
+ execution_plan = planner.create_execution_plan(switch_results)
2925
+
2926
+ # Cache the result (with size limit)
2927
+ if len(self._execution_plan_cache) >= 100: # Limit cache size
2928
+ # Remove oldest entries (simple FIFO)
2929
+ oldest_key = next(iter(self._execution_plan_cache))
2930
+ del self._execution_plan_cache[oldest_key]
2931
+
2932
+ self._execution_plan_cache[cache_key] = execution_plan
2933
+
2934
+ except Exception as e:
2935
+ self.logger.warning(f"Error creating cached execution plan: {e}")
2936
+ # Fallback to basic topological order
2937
+ execution_plan = list(nx.topological_sort(workflow.graph))
2938
+
2939
+ return execution_plan
2940
+
2941
+ def _create_execution_plan_cache_key(
2942
+ self, workflow: Workflow, switch_results: Dict[str, Dict[str, Any]]
2943
+ ) -> str:
2944
+ """
2945
+ Create cache key for execution plan.
2946
+
2947
+ Args:
2948
+ workflow: Workflow instance
2949
+ switch_results: SwitchNode results
2950
+
2951
+ Returns:
2952
+ Cache key string
2953
+ """
2954
+ import json
2955
+
2956
+ try:
2957
+ # Create key from workflow structure + switch results
2958
+ workflow_key = f"{workflow.workflow_id}_{len(workflow.graph.nodes)}_{len(workflow.graph.edges)}"
2959
+
2960
+ # Sort switch results for consistent caching
2961
+ sorted_results = {}
2962
+ for switch_id, result in switch_results.items():
2963
+ if isinstance(result, dict):
2964
+ # Create deterministic representation
2965
+ sorted_results[switch_id] = {
2966
+ k: v
2967
+ for k, v in sorted(result.items())
2968
+ if k in ["true_output", "false_output", "condition_result"]
2969
+ }
2970
+
2971
+ results_str = json.dumps(sorted_results, sort_keys=True, default=str)
2972
+ combined_key = f"{workflow_key}:{results_str}"
2973
+
2974
+ # Hash to fixed length
2975
+ return hashlib.md5(combined_key.encode()).hexdigest()
2976
+
2977
+ except Exception as e:
2978
+ self.logger.warning(f"Error creating cache key: {e}")
2979
+ # Fallback to simple key
2980
+ return f"{workflow.workflow_id}_{hash(str(switch_results))}"
2981
+
2982
+ def get_execution_analytics(self) -> Dict[str, Any]:
2983
+ """
2984
+ Get comprehensive execution analytics for monitoring and optimization.
2985
+
2986
+ Returns:
2987
+ Dictionary containing detailed analytics data
2988
+ """
2989
+ analytics = {
2990
+ "cache_performance": {
2991
+ "hits": self._analytics_data["cache_hits"],
2992
+ "misses": self._analytics_data["cache_misses"],
2993
+ "hit_rate": self._analytics_data["cache_hits"]
2994
+ / max(
2995
+ 1,
2996
+ self._analytics_data["cache_hits"]
2997
+ + self._analytics_data["cache_misses"],
2998
+ ),
2999
+ },
3000
+ "conditional_execution_stats": {
3001
+ "total_executions": len(self._analytics_data["conditional_executions"]),
3002
+ "average_performance_improvement": 0.0,
3003
+ "fallback_rate": 0.0,
3004
+ },
3005
+ "performance_history": self._analytics_data["performance_history"][
3006
+ -50:
3007
+ ], # Last 50 executions
3008
+ "execution_patterns": self._analytics_data["execution_patterns"],
3009
+ "optimization_stats": self._analytics_data["optimization_stats"],
3010
+ }
3011
+
3012
+ # Calculate conditional execution statistics
3013
+ if self._analytics_data["conditional_executions"]:
3014
+ improvements = [
3015
+ exec_data.get("performance_improvement", 0)
3016
+ for exec_data in self._analytics_data["conditional_executions"]
3017
+ ]
3018
+ analytics["conditional_execution_stats"][
3019
+ "average_performance_improvement"
3020
+ ] = sum(improvements) / len(improvements)
3021
+
3022
+ fallbacks = sum(
3023
+ 1
3024
+ for exec_data in self._analytics_data["conditional_executions"]
3025
+ if exec_data.get("used_fallback", False)
3026
+ )
3027
+ analytics["conditional_execution_stats"]["fallback_rate"] = fallbacks / len(
3028
+ self._analytics_data["conditional_executions"]
3029
+ )
3030
+
3031
+ # Add cache statistics
3032
+ cache_size = len(self._execution_plan_cache)
3033
+ analytics["cache_performance"]["cache_size"] = cache_size
3034
+ analytics["cache_performance"]["cache_efficiency"] = min(
3035
+ 1.0, cache_size / 100.0
3036
+ ) # Relative to max size
3037
+
3038
+ return analytics
3039
+
3040
+ def record_execution_performance(
3041
+ self,
3042
+ workflow: Workflow,
3043
+ execution_time: float,
3044
+ nodes_executed: int,
3045
+ used_conditional: bool,
3046
+ performance_improvement: float = 0.0,
3047
+ ):
3048
+ """
3049
+ Record execution performance for analytics.
3050
+
3051
+ Args:
3052
+ workflow: Workflow that was executed
3053
+ execution_time: Total execution time in seconds
3054
+ nodes_executed: Number of nodes actually executed
3055
+ used_conditional: Whether conditional execution was used
3056
+ performance_improvement: Performance improvement percentage (0.0-1.0)
3057
+ """
3058
+ import time
3059
+
3060
+ performance_record = {
3061
+ "timestamp": time.time(),
3062
+ "workflow_id": workflow.workflow_id,
3063
+ "workflow_name": workflow.name,
3064
+ "total_nodes": len(workflow.graph.nodes),
3065
+ "executed_nodes": nodes_executed,
3066
+ "execution_time": execution_time,
3067
+ "used_conditional_execution": used_conditional,
3068
+ "performance_improvement": performance_improvement,
3069
+ "nodes_per_second": nodes_executed / max(0.001, execution_time),
3070
+ }
3071
+
3072
+ # Add to performance history
3073
+ self._analytics_data["performance_history"].append(performance_record)
3074
+
3075
+ # Limit history size
3076
+ if len(self._analytics_data["performance_history"]) > 1000:
3077
+ self._analytics_data["performance_history"] = self._analytics_data[
3078
+ "performance_history"
3079
+ ][-500:]
3080
+
3081
+ # Record conditional execution if used
3082
+ if used_conditional:
3083
+ self._analytics_data["conditional_executions"].append(
3084
+ {
3085
+ "timestamp": time.time(),
3086
+ "workflow_id": workflow.workflow_id,
3087
+ "performance_improvement": performance_improvement,
3088
+ "nodes_skipped": len(workflow.graph.nodes) - nodes_executed,
3089
+ "used_fallback": False, # Set by fallback tracking
3090
+ }
3091
+ )
3092
+
3093
+ # Update execution patterns
3094
+ pattern_key = f"{len(workflow.graph.nodes)}_nodes"
3095
+ if pattern_key not in self._analytics_data["execution_patterns"]:
3096
+ self._analytics_data["execution_patterns"][pattern_key] = {
3097
+ "count": 0,
3098
+ "avg_execution_time": 0.0,
3099
+ "avg_performance_improvement": 0.0,
3100
+ }
3101
+
3102
+ pattern = self._analytics_data["execution_patterns"][pattern_key]
3103
+ pattern["count"] += 1
3104
+ pattern["avg_execution_time"] = (
3105
+ pattern["avg_execution_time"] * (pattern["count"] - 1) + execution_time
3106
+ ) / pattern["count"]
3107
+ if used_conditional:
3108
+ pattern["avg_performance_improvement"] = (
3109
+ pattern["avg_performance_improvement"] * (pattern["count"] - 1)
3110
+ + performance_improvement
3111
+ ) / pattern["count"]
3112
+
3113
+ def clear_analytics_data(self, keep_patterns: bool = True):
3114
+ """
3115
+ Clear analytics data for fresh monitoring.
3116
+
3117
+ Args:
3118
+ keep_patterns: Whether to preserve execution patterns
3119
+ """
3120
+ self._analytics_data["conditional_executions"] = []
3121
+ self._analytics_data["performance_history"] = []
3122
+ self._analytics_data["cache_hits"] = 0
3123
+ self._analytics_data["cache_misses"] = 0
3124
+
3125
+ if not keep_patterns:
3126
+ self._analytics_data["execution_patterns"] = {}
3127
+ self._analytics_data["optimization_stats"] = {}
3128
+
3129
+ # Clear caches
3130
+ self._execution_plan_cache.clear()
3131
+
3132
+ self.logger.info("Analytics data cleared")
3133
+
3134
+ def get_health_diagnostics(self) -> Dict[str, Any]:
3135
+ """
3136
+ Get health diagnostics for monitoring system health.
3137
+
3138
+ Returns:
3139
+ Dictionary containing health check results
3140
+ """
3141
+ import os
3142
+ import time
3143
+
3144
+ diagnostics = {
3145
+ "timestamp": time.time(),
3146
+ "runtime_health": "healthy",
3147
+ "cache_health": "healthy",
3148
+ "performance_health": "healthy",
3149
+ "memory_usage": {},
3150
+ "cache_statistics": {},
3151
+ "performance_indicators": {},
3152
+ "warnings": [],
3153
+ "errors": [],
3154
+ }
3155
+
3156
+ try:
3157
+ # Memory usage
3158
+ process = psutil.Process(os.getpid())
3159
+ memory_info = process.memory_info()
3160
+ diagnostics["memory_usage"] = {
3161
+ "rss_mb": memory_info.rss / 1024 / 1024,
3162
+ "vms_mb": memory_info.vms / 1024 / 1024,
3163
+ "percent": process.memory_percent(),
3164
+ }
3165
+
3166
+ # Cache health
3167
+ cache_size = len(self._execution_plan_cache)
3168
+ analytics = self.get_execution_analytics()
3169
+ cache_hit_rate = analytics["cache_performance"]["hit_rate"]
3170
+
3171
+ diagnostics["cache_statistics"] = {
3172
+ "size": cache_size,
3173
+ "hit_rate": cache_hit_rate,
3174
+ "hits": analytics["cache_performance"]["hits"],
3175
+ "misses": analytics["cache_performance"]["misses"],
3176
+ }
3177
+
3178
+ # Performance indicators
3179
+ recent_executions = self._analytics_data["performance_history"][-10:]
3180
+ if recent_executions:
3181
+ avg_execution_time = sum(
3182
+ e["execution_time"] for e in recent_executions
3183
+ ) / len(recent_executions)
3184
+ avg_improvement = sum(
3185
+ e["performance_improvement"] for e in recent_executions
3186
+ ) / len(recent_executions)
3187
+
3188
+ diagnostics["performance_indicators"] = {
3189
+ "avg_execution_time": avg_execution_time,
3190
+ "avg_performance_improvement": avg_improvement,
3191
+ "recent_executions": len(recent_executions),
3192
+ }
3193
+
3194
+ # Health checks
3195
+ if (
3196
+ cache_hit_rate < 0.3
3197
+ and analytics["cache_performance"]["hits"]
3198
+ + analytics["cache_performance"]["misses"]
3199
+ > 10
3200
+ ):
3201
+ diagnostics["warnings"].append(
3202
+ "Low cache hit rate - consider workflow optimization"
3203
+ )
3204
+ diagnostics["cache_health"] = "warning"
3205
+
3206
+ if diagnostics["memory_usage"]["percent"] > 80:
3207
+ diagnostics["warnings"].append("High memory usage detected")
3208
+ diagnostics["runtime_health"] = "warning"
3209
+
3210
+ if recent_executions and avg_execution_time > 5.0:
3211
+ diagnostics["warnings"].append("Slow execution times detected")
3212
+ diagnostics["performance_health"] = "warning"
3213
+
3214
+ except Exception as e:
3215
+ diagnostics["errors"].append(f"Health check error: {e}")
3216
+ diagnostics["runtime_health"] = "error"
3217
+
3218
+ return diagnostics
3219
+
3220
+ def optimize_runtime_performance(self) -> Dict[str, Any]:
3221
+ """
3222
+ Optimize runtime performance based on analytics data.
3223
+
3224
+ Returns:
3225
+ Dictionary describing optimizations applied
3226
+ """
3227
+ optimization_result = {
3228
+ "optimizations_applied": [],
3229
+ "performance_impact": {},
3230
+ "recommendations": [],
3231
+ "cache_optimizations": {},
3232
+ "memory_optimizations": {},
3233
+ }
3234
+
3235
+ try:
3236
+ # Cache optimization
3237
+ cache_analytics = self.get_execution_analytics()["cache_performance"]
3238
+
3239
+ if (
3240
+ cache_analytics["hit_rate"] < 0.5
3241
+ and cache_analytics["hits"] + cache_analytics["misses"] > 20
3242
+ ):
3243
+ # Poor cache performance - clear and rebuild
3244
+ old_size = len(self._execution_plan_cache)
3245
+ self._execution_plan_cache.clear()
3246
+ optimization_result["optimizations_applied"].append("cache_clear")
3247
+ optimization_result["cache_optimizations"]["cleared_entries"] = old_size
3248
+ optimization_result["recommendations"].append(
3249
+ "Consider using more consistent workflows for better caching"
3250
+ )
3251
+
3252
+ # Memory optimization
3253
+ if len(self._analytics_data["performance_history"]) > 500:
3254
+ old_count = len(self._analytics_data["performance_history"])
3255
+ self._analytics_data["performance_history"] = self._analytics_data[
3256
+ "performance_history"
3257
+ ][-250:]
3258
+ optimization_result["optimizations_applied"].append("history_cleanup")
3259
+ optimization_result["memory_optimizations"][
3260
+ "history_entries_removed"
3261
+ ] = (old_count - 250)
3262
+
3263
+ # Execution pattern analysis
3264
+ patterns = self._analytics_data["execution_patterns"]
3265
+ if patterns:
3266
+ most_common_pattern = max(patterns.items(), key=lambda x: x[1]["count"])
3267
+ optimization_result["recommendations"].append(
3268
+ f"Most common pattern: {most_common_pattern[0]} with {most_common_pattern[1]['count']} executions"
3269
+ )
3270
+
3271
+ # Suggest optimizations based on patterns
3272
+ for pattern_key, pattern_data in patterns.items():
3273
+ if pattern_data["avg_execution_time"] > 3.0:
3274
+ optimization_result["recommendations"].append(
3275
+ f"Consider optimizing workflows with {pattern_key} - avg time: {pattern_data['avg_execution_time']:.2f}s"
3276
+ )
3277
+
3278
+ self.logger.info(
3279
+ f"Runtime optimization completed: {len(optimization_result['optimizations_applied'])} optimizations applied"
3280
+ )
3281
+
3282
+ except Exception as e:
3283
+ self.logger.warning(f"Error during runtime optimization: {e}")
3284
+ optimization_result["error"] = str(e)
3285
+
3286
+ return optimization_result
3287
+
3288
+ # ===== PHASE 3 COMPLETION: Performance Monitoring & Compatibility =====
3289
+
3290
+ def _check_performance_switch(self, current_mode: str) -> Tuple[bool, str, str]:
3291
+ """Check if execution mode should be switched based on performance.
3292
+
3293
+ Args:
3294
+ current_mode: Current execution mode
3295
+
3296
+ Returns:
3297
+ Tuple of (should_switch, recommended_mode, reason)
3298
+ """
3299
+ # Initialize performance monitor if needed
3300
+ if self._performance_monitor is None:
3301
+ self._performance_monitor = PerformanceMonitor()
3302
+
3303
+ return self._performance_monitor.should_switch_mode(current_mode)
3304
+
3305
+ def _record_execution_metrics(
3306
+ self,
3307
+ workflow: Workflow,
3308
+ execution_time: float,
3309
+ node_count: int,
3310
+ skipped_nodes: int,
3311
+ execution_mode: str,
3312
+ ) -> None:
3313
+ """Record execution metrics for performance monitoring.
3314
+
3315
+ Args:
3316
+ workflow: Executed workflow
3317
+ execution_time: Total execution time
3318
+ node_count: Number of nodes executed
3319
+ skipped_nodes: Number of nodes skipped
3320
+ execution_mode: Execution mode used
3321
+ """
3322
+ if not self._enable_performance_monitoring:
3323
+ return
3324
+
3325
+ # Initialize performance monitor if needed
3326
+ if self._performance_monitor is None:
3327
+ self._performance_monitor = PerformanceMonitor()
3328
+
3329
+ metrics = ExecutionMetrics(
3330
+ execution_time=execution_time,
3331
+ node_count=node_count,
3332
+ skipped_nodes=skipped_nodes,
3333
+ execution_mode=execution_mode,
3334
+ )
3335
+
3336
+ self._performance_monitor.record_execution(metrics)
3337
+
3338
+ def get_performance_report(self) -> Dict[str, Any]:
3339
+ """Get performance monitoring report.
3340
+
3341
+ Returns:
3342
+ Performance statistics and recommendations
3343
+ """
3344
+ if self._performance_monitor is None:
3345
+ return {"status": "Performance monitoring not initialized"}
3346
+
3347
+ return self._performance_monitor.get_performance_report()
3348
+
3349
+ def generate_compatibility_report(self, workflow: Workflow) -> Dict[str, Any]:
3350
+ """Generate compatibility report for a workflow.
3351
+
3352
+ Args:
3353
+ workflow: Workflow to analyze
3354
+
3355
+ Returns:
3356
+ Compatibility report dictionary
3357
+ """
3358
+ if not self._enable_compatibility_reporting:
3359
+ return {"status": "Compatibility reporting disabled"}
3360
+
3361
+ # Initialize reporter if needed
3362
+ if self._compatibility_reporter is None:
3363
+ self._compatibility_reporter = CompatibilityReporter()
3364
+
3365
+ report = self._compatibility_reporter.analyze_workflow(workflow)
3366
+ return report.to_dict()
3367
+
3368
+ def get_compatibility_report_markdown(self, workflow: Workflow) -> str:
3369
+ """Generate compatibility report in markdown format.
3370
+
3371
+ Args:
3372
+ workflow: Workflow to analyze
3373
+
3374
+ Returns:
3375
+ Markdown formatted report
3376
+ """
3377
+ if not self._enable_compatibility_reporting:
3378
+ return "# Compatibility reporting disabled"
3379
+
3380
+ # Initialize reporter if needed
3381
+ if self._compatibility_reporter is None:
3382
+ self._compatibility_reporter = CompatibilityReporter()
3383
+
3384
+ report = self._compatibility_reporter.analyze_workflow(workflow)
3385
+ return report.to_markdown()
3386
+
3387
+ def set_performance_monitoring(self, enabled: bool) -> None:
3388
+ """Enable or disable performance monitoring.
3389
+
3390
+ Args:
3391
+ enabled: Whether to enable performance monitoring
3392
+ """
3393
+ self._enable_performance_monitoring = enabled
3394
+ self.logger.info(
3395
+ f"Performance monitoring {'enabled' if enabled else 'disabled'}"
3396
+ )
3397
+
3398
+ def set_automatic_mode_switching(self, enabled: bool) -> None:
3399
+ """Enable or disable automatic mode switching based on performance.
3400
+
3401
+ Args:
3402
+ enabled: Whether to enable automatic switching
3403
+ """
3404
+ self._performance_switch_enabled = enabled
3405
+ self.logger.info(
3406
+ f"Automatic mode switching {'enabled' if enabled else 'disabled'}"
3407
+ )
3408
+
3409
+ def set_compatibility_reporting(self, enabled: bool) -> None:
3410
+ """Enable or disable compatibility reporting.
3411
+
3412
+ Args:
3413
+ enabled: Whether to enable compatibility reporting
3414
+ """
3415
+ self._enable_compatibility_reporting = enabled
3416
+ self.logger.info(
3417
+ f"Compatibility reporting {'enabled' if enabled else 'disabled'}"
3418
+ )
3419
+
3420
+ def get_execution_path_debug_info(self) -> Dict[str, Any]:
3421
+ """Get detailed debug information about execution paths.
3422
+
3423
+ Returns:
3424
+ Debug information including execution decisions and paths
3425
+ """
3426
+ debug_info = {
3427
+ "conditional_execution_mode": self.conditional_execution,
3428
+ "performance_monitoring_enabled": self._enable_performance_monitoring,
3429
+ "automatic_switching_enabled": self._performance_switch_enabled,
3430
+ "compatibility_reporting_enabled": self._enable_compatibility_reporting,
3431
+ "fallback_metrics": self._fallback_metrics,
3432
+ "execution_analytics": self.get_execution_analytics(),
3433
+ }
3434
+
3435
+ if self._performance_monitor:
3436
+ debug_info["performance_report"] = self.get_performance_report()
3437
+
3438
+ return debug_info