kailash 0.8.6__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -85,10 +85,14 @@ class SwitchNode(Node):
85
85
  ... operator="==",
86
86
  ... value=True
87
87
  ... ))
88
- >>> workflow.connect("convergence", "switch")
89
- >>> workflow.connect("switch", "processor",
90
- ... condition="false_output", cycle=True)
91
- >>> workflow.connect("switch", "output", condition="true_output")
88
+ >>> workflow.add_connection("convergence", "result", "switch", "input_data")
89
+ >>> # Use CycleBuilder for cyclic connections
90
+ >>> cycle = workflow.create_cycle("convergence_loop")
91
+ >>> cycle.connect("switch", "false_output", "processor", "input")
92
+ >>> cycle.connect("processor", "result", "convergence", "data")
93
+ >>> cycle.max_iterations(50).build()
94
+ >>> # Non-cyclic output connection
95
+ >>> workflow.add_connection("switch", "true_output", "output", "data")
92
96
  """
93
97
 
94
98
  def get_parameters(self) -> dict[str, NodeParameter]:
kailash/runtime/local.py CHANGED
@@ -636,6 +636,26 @@ class LocalRuntime:
636
636
  if self.debug:
637
637
  self.logger.debug(f"Node {node_id} inputs: {inputs}")
638
638
 
639
+ # CONDITIONAL EXECUTION: Skip nodes that only receive None inputs from conditional routing
640
+ if self._should_skip_conditional_node(workflow, node_id, inputs):
641
+ self.logger.info(
642
+ f"Skipping node {node_id} - all conditional inputs are None"
643
+ )
644
+ # Store None result to indicate the node was skipped
645
+ results[node_id] = None
646
+ node_outputs[node_id] = None
647
+
648
+ # Update task status if tracking is enabled
649
+ if task and task_manager:
650
+ task_manager.update_task_status(
651
+ task.task_id,
652
+ TaskStatus.COMPLETED,
653
+ result=None,
654
+ ended_at=datetime.now(UTC),
655
+ metadata={"skipped": True, "reason": "conditional_routing"},
656
+ )
657
+ continue
658
+
639
659
  # Execute node with unified async/sync support and metrics collection
640
660
  collector = MetricsCollector()
641
661
  with collector.collect(node_id=node_id) as metrics_context:
@@ -1123,6 +1143,88 @@ class LocalRuntime:
1123
1143
  metrics_collector = get_metrics_collector()
1124
1144
  metrics_collector.reset_metrics()
1125
1145
 
1146
+ def _should_skip_conditional_node(
1147
+ self, workflow: Workflow, node_id: str, inputs: dict[str, Any]
1148
+ ) -> bool:
1149
+ """Determine if a node should be skipped due to conditional routing.
1150
+
1151
+ A node should be skipped if:
1152
+ 1. It has incoming connections from conditional nodes (like SwitchNode)
1153
+ 2. All of its connected inputs are None
1154
+ 3. It has no node-level configuration parameters that would make it run independently
1155
+
1156
+ Args:
1157
+ workflow: The workflow being executed.
1158
+ node_id: Node ID to check.
1159
+ inputs: Prepared inputs for the node.
1160
+
1161
+ Returns:
1162
+ True if the node should be skipped, False otherwise.
1163
+ """
1164
+ # Get all incoming edges for this node
1165
+ incoming_edges = list(workflow.graph.in_edges(node_id, data=True))
1166
+
1167
+ # If the node has no incoming connections, don't skip it
1168
+ # (it might be a source node or have configuration parameters)
1169
+ if not incoming_edges:
1170
+ return False
1171
+
1172
+ # Check if any incoming edges are from conditional nodes
1173
+ has_conditional_inputs = False
1174
+ for source_node_id, _, edge_data in incoming_edges:
1175
+ source_node = workflow._node_instances.get(source_node_id)
1176
+ if source_node and source_node.__class__.__name__ in ["SwitchNode"]:
1177
+ has_conditional_inputs = True
1178
+ break
1179
+
1180
+ # If no conditional inputs, don't skip
1181
+ if not has_conditional_inputs:
1182
+ return False
1183
+
1184
+ # Get the node instance to check for configuration parameters
1185
+ node_instance = workflow._node_instances.get(node_id)
1186
+ if not node_instance:
1187
+ return False
1188
+
1189
+ # Check if the node has configuration parameters that would make it run independently
1190
+ # (excluding standard parameters and None values)
1191
+ node_config = getattr(node_instance, "config", {})
1192
+ significant_config = {
1193
+ k: v
1194
+ for k, v in node_config.items()
1195
+ if k not in ["metadata", "name", "id"] and v is not None
1196
+ }
1197
+
1198
+ # If the node has significant configuration, it might still be valuable to run
1199
+ if significant_config:
1200
+ # Check if any connected inputs have actual data (not None)
1201
+ connected_inputs = {}
1202
+ for _, _, edge_data in incoming_edges:
1203
+ mapping = edge_data.get("mapping", {})
1204
+ for source_key, target_key in mapping.items():
1205
+ if target_key in inputs:
1206
+ connected_inputs[target_key] = inputs[target_key]
1207
+
1208
+ # If all connected inputs are None but node has config, still skip
1209
+ # The user can configure the node to run with default values if needed
1210
+ if all(v is None for v in connected_inputs.values()):
1211
+ return True
1212
+
1213
+ # Check if all connected inputs are None
1214
+ # This is the main condition for conditional routing
1215
+ has_non_none_input = False
1216
+ for _, _, edge_data in incoming_edges:
1217
+ mapping = edge_data.get("mapping", {})
1218
+ 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
1224
+
1225
+ # Skip the node if all connected inputs are None
1226
+ return not has_non_none_input
1227
+
1126
1228
  def _should_stop_on_error(self, workflow: Workflow, node_id: str) -> bool:
1127
1229
  """Determine if execution should stop when a node fails.
1128
1230
 
@@ -220,6 +220,60 @@ class CyclicWorkflowExecutor:
220
220
  else:
221
221
  return obj
222
222
 
223
+ def _should_skip_conditional_node_cyclic(
224
+ self, workflow: Workflow, node_id: str, merged_inputs: dict[str, Any]
225
+ ) -> bool:
226
+ """Determine if a node should be skipped due to conditional routing in cyclic execution.
227
+
228
+ This is similar to LocalRuntime._should_skip_conditional_node but adapted for cyclic execution.
229
+
230
+ Args:
231
+ workflow: The workflow being executed.
232
+ node_id: Node ID to check.
233
+ merged_inputs: Merged inputs for the node.
234
+
235
+ Returns:
236
+ True if the node should be skipped, False otherwise.
237
+ """
238
+ # Get all incoming edges for this node
239
+ incoming_edges = list(workflow.graph.in_edges(node_id, data=True))
240
+
241
+ # If the node has no incoming connections, don't skip it
242
+ if not incoming_edges:
243
+ return False
244
+
245
+ # Check if any incoming edges are from conditional nodes
246
+ has_conditional_inputs = False
247
+ for source_node_id, _, edge_data in incoming_edges:
248
+ try:
249
+ source_node = workflow.get_node(source_node_id)
250
+ if source_node and source_node.__class__.__name__ in ["SwitchNode"]:
251
+ has_conditional_inputs = True
252
+ break
253
+ except:
254
+ continue
255
+
256
+ # If no conditional inputs, don't skip
257
+ if not has_conditional_inputs:
258
+ return False
259
+
260
+ # Check if all connected inputs are None
261
+ has_non_none_input = False
262
+ for _, _, edge_data in incoming_edges:
263
+ mapping = edge_data.get("mapping", {})
264
+ for source_key, target_key in mapping.items():
265
+ if (
266
+ target_key in merged_inputs
267
+ and merged_inputs[target_key] is not None
268
+ ):
269
+ has_non_none_input = True
270
+ break
271
+ if has_non_none_input:
272
+ break
273
+
274
+ # Skip the node if all connected inputs are None
275
+ return not has_non_none_input
276
+
223
277
  def _execute_with_cycles(
224
278
  self,
225
279
  workflow: Workflow,
@@ -863,6 +917,20 @@ class CyclicWorkflowExecutor:
863
917
  except Exception as e:
864
918
  logger.warning(f"Failed to create task for node '{node_id}': {e}")
865
919
 
920
+ # CONDITIONAL EXECUTION: Skip nodes that only receive None inputs from conditional routing
921
+ if self._should_skip_conditional_node_cyclic(workflow, node_id, merged_inputs):
922
+ logger.info(f"Skipping node {node_id} - all conditional inputs are None")
923
+ # Store None result to indicate the node was skipped
924
+ if task and task_manager:
925
+ task_manager.update_task_status(
926
+ task.task_id,
927
+ TaskStatus.COMPLETED,
928
+ result=None,
929
+ ended_at=datetime.now(UTC),
930
+ metadata={"skipped": True, "reason": "conditional_routing"},
931
+ )
932
+ return None
933
+
866
934
  # Execute node with metrics collection
867
935
  collector = MetricsCollector()
868
936
  logger.debug(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kailash
3
- Version: 0.8.6
3
+ Version: 0.9.0
4
4
  Summary: Python SDK for the Kailash container-node architecture
5
5
  Home-page: https://github.com/integrum/kailash-python-sdk
6
6
  Author: Integrum
@@ -81,6 +81,7 @@ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.55b1
81
81
  Requires-Dist: seaborn>=0.13.2
82
82
  Requires-Dist: sqlparse>=0.5.3
83
83
  Requires-Dist: jsonschema>=4.24.0
84
+ Requires-Dist: openai>=1.97.1
84
85
  Provides-Extra: dev
85
86
  Requires-Dist: pytest>=7.0; extra == "dev"
86
87
  Requires-Dist: pytest-cov>=3.0; extra == "dev"
@@ -1,4 +1,4 @@
1
- kailash/__init__.py,sha256=OnMcVOw8TgoGUth6Z_dyWPrlXQV4pECdOA5GYsfelEI,2628
1
+ kailash/__init__.py,sha256=c9Upj65mN4RX9QCF8NliTUtNaZJJW8jiJqBvR4c6vmA,2537
2
2
  kailash/__main__.py,sha256=vr7TVE5o16V6LsTmRFKG6RDKUXHpIWYdZ6Dok2HkHnI,198
3
3
  kailash/access_control.py,sha256=MjKtkoQ2sg1Mgfe7ovGxVwhAbpJKvaepPWr8dxOueMA,26058
4
4
  kailash/access_control_abac.py,sha256=FPfa_8PuDP3AxTjdWfiH3ntwWO8NodA0py9W8SE5dno,30263
@@ -88,9 +88,10 @@ kailash/mcp_server/client_new.py,sha256=YU671JvAM0uvuX0uhGZCIKI8co3fqz0cs6HqLZ59
88
88
  kailash/mcp_server/discovery.py,sha256=D8vcwVkbgQCNp0_BlkGeU_dnqgIXN2g0s3_GpndlQu0,57828
89
89
  kailash/mcp_server/errors.py,sha256=_lycwudWP_AJ_KwLO5N3VCKbG1ikfaTyzA2PBF8UAYU,21181
90
90
  kailash/mcp_server/oauth.py,sha256=GFC2O2ueiTTI6V-91Huevhc3K8CxrHe22knuHfuCTqY,56493
91
- kailash/mcp_server/protocol.py,sha256=gXeJ-GvSf39WymfS6433SqLKBA40PdeDoBQqu7DUbJE,35129
91
+ kailash/mcp_server/protocol.py,sha256=NIdEwJT21JT9ItajXniPNvCbZtTbpqyOC_ZezqsguGE,35694
92
92
  kailash/mcp_server/registry_integration.py,sha256=B8CSLq_O1ea3cXrbVjC3bB_OFgHIP-KS9dk77mNM02I,19791
93
- kailash/mcp_server/server.py,sha256=RQchBc-yaz5Mf_ZRrHukv8IGGgwKKXTg0hEtaVLGeo4,80246
93
+ kailash/mcp_server/server.py,sha256=yFp1F4QQl6gkTY_9JJWmiMiwfT-zACLJLubz-NR5sCw,108675
94
+ kailash/mcp_server/subscriptions.py,sha256=J0FXg6_8lOffQ5SSwm2_DmqIS2pOii6ikxORANLCy1o,58589
94
95
  kailash/mcp_server/transports.py,sha256=fBa7CTVYTDb0ZbBQTsZ2d8rKvcVuqBIteczq8eqarr4,49919
95
96
  kailash/mcp_server/servers/ai_registry.py,sha256=IdF_keUuJlMsvjLjSAykxxbm46K4qA7eCj7T-lYSrzk,10007
96
97
  kailash/mcp_server/utils/__init__.py,sha256=R20N-iiKXUPxc9MOh6vPO1vIfkPmwhEQ5KNFgGd4xSs,771
@@ -247,7 +248,7 @@ kailash/nodes/logic/__init__.py,sha256=JKGFXwBDfY3s1MWQkx3ivdvCMm3b3HIXCn-wH9uMo
247
248
  kailash/nodes/logic/async_operations.py,sha256=bpCc-t5uKi4DVJiVdXUcIZ6BWJOLsxQu-4UCEnwpw6M,27509
248
249
  kailash/nodes/logic/convergence.py,sha256=ooNMdEJs8B0SeMQrzU1Uo_iPiuPXi8j-9IyRDSl9sYM,25080
249
250
  kailash/nodes/logic/loop.py,sha256=34hnrcfeigcpsVcomsd-ZLE2x7f3irAd_-Q89vZzW9w,5756
250
- kailash/nodes/logic/operations.py,sha256=dC594biBTdiw3vbyeu3TgPZs7UYAfFjbXGwcYbN_5ec,28722
251
+ kailash/nodes/logic/operations.py,sha256=eJpTCWXqOIyP7jc057CXkhW2CBpTIVZXGkV74LUgza4,29002
251
252
  kailash/nodes/logic/workflow.py,sha256=p2ED6tOWGVC50iNyUInSpJI41eBXmSF8Tb_w0h7NeD0,17136
252
253
  kailash/nodes/mixins/__init__.py,sha256=0WYfu5kj-lHbFwP9g5vmlbsG8UzvI-vhOyHMEUzXbz4,558
253
254
  kailash/nodes/mixins/event_emitter.py,sha256=xTeNrBWmuWIf8qYA5DZekymjjrTAD1sboW9dKbAP40w,7492
@@ -316,7 +317,7 @@ kailash/runtime/__init__.py,sha256=CvU-qBMESYYISqFOlYlLsYJrXJu0Gqr4x6yr4Ob_Rng,2
316
317
  kailash/runtime/access_controlled.py,sha256=HtNJZylaB-2FuPsfEOfQ-4ny4HzwJfHaHNMu2xS1Nzs,17324
317
318
  kailash/runtime/async_local.py,sha256=sYNggSU0R-oo8cCvU5ayodDBqASzUhxu994ZvZxDSC0,34010
318
319
  kailash/runtime/docker.py,sha256=sZknVl1PCGfAZeyc0-exTuKlllSyjYlFIgJoiB3CRNs,23500
319
- kailash/runtime/local.py,sha256=lDE36rIL1Pl1SjDkDStWMlF3qeaZRQzGugkWtcoMQRc,66013
320
+ kailash/runtime/local.py,sha256=gWHuSgbPWqIHYvirRo80LV8RjDvOR2PQoUs737_pAh0,70485
320
321
  kailash/runtime/parallel.py,sha256=mz_wPD13-YVc3Q_8HkOs4nPQPdTjnjCcnRL7ZRM70lo,21070
321
322
  kailash/runtime/parallel_cyclic.py,sha256=yANZHnePjhCPuCFbq3lFQA1K6jbCv5Of5-vIKbCsmZk,19863
322
323
  kailash/runtime/parameter_injection.py,sha256=kG4GhmarsRr5t3VDFbc2G1HSbsZJg6UmienHCE2Ru7o,14852
@@ -379,7 +380,7 @@ kailash/workflow/cycle_debugger.py,sha256=eG-Q_kakqyhr1Ts-q4pRnO0EI7mVO9ao1s9WOx
379
380
  kailash/workflow/cycle_exceptions.py,sha256=4_OLnbEXqIiXKzOc3uh8DzFik4wEHwl8bRZhY9Xhf2A,21838
380
381
  kailash/workflow/cycle_profiler.py,sha256=aEWSCm0Xy15SjgLTpPooVJMzpFhtJWt4livR-3Me4N8,28547
381
382
  kailash/workflow/cycle_state.py,sha256=hzRUvciRreWfS56Cf7ZLQPit_mlPTQDoNTawh8yi-2s,10747
382
- kailash/workflow/cyclic_runner.py,sha256=0IlmghTrwYb3ivqP975DprP3aj45-8_Sn5Wq9tEutG0,43888
383
+ kailash/workflow/cyclic_runner.py,sha256=CFr4eucYrN_9K1XwiTE_ZmtbpdwDJv1OA7g_Pc0hEAI,46616
383
384
  kailash/workflow/edge_infrastructure.py,sha256=lQDzs0-KdoCMqI4KAXAGbhHbwadM6t-ffJEWLlRuSNo,12448
384
385
  kailash/workflow/graph.py,sha256=zRpGLXeuwtuxFBvE7_16c_bB9yqZirM_uwtfD1_MY4g,59272
385
386
  kailash/workflow/input_handling.py,sha256=HrW--AmelYC8F18nkfmYlF_wXycA24RuNbDRjvM8rqk,6561
@@ -394,9 +395,9 @@ kailash/workflow/templates.py,sha256=XQMAKZXC2dlxgMMQhSEOWAF3hIbe9JJt9j_THchhAm8
394
395
  kailash/workflow/type_inference.py,sha256=i1F7Yd_Z3elTXrthsLpqGbOnQBIVVVEjhRpI0HrIjd0,24492
395
396
  kailash/workflow/validation.py,sha256=r2zApGiiG8UEn7p5Ji842l8OR1_KftzDkWc7gg0cac0,44675
396
397
  kailash/workflow/visualization.py,sha256=nHBW-Ai8QBMZtn2Nf3EE1_aiMGi9S6Ui_BfpA5KbJPU,23187
397
- kailash-0.8.6.dist-info/licenses/LICENSE,sha256=Axe6g7bTrJkToK9h9j2SpRUKKNaDZDCo2lQ2zPxCE6s,1065
398
- kailash-0.8.6.dist-info/METADATA,sha256=8CMVRW3iAN5K_8U_1GN8OxLJF4HGjoo8G0vzKMq7kRc,21703
399
- kailash-0.8.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
400
- kailash-0.8.6.dist-info/entry_points.txt,sha256=M_q3b8PG5W4XbhSgESzIJjh3_4OBKtZFYFsOdkr2vO4,45
401
- kailash-0.8.6.dist-info/top_level.txt,sha256=z7GzH2mxl66498pVf5HKwo5wwfPtt9Aq95uZUpH6JV0,8
402
- kailash-0.8.6.dist-info/RECORD,,
398
+ kailash-0.9.0.dist-info/licenses/LICENSE,sha256=Axe6g7bTrJkToK9h9j2SpRUKKNaDZDCo2lQ2zPxCE6s,1065
399
+ kailash-0.9.0.dist-info/METADATA,sha256=h2b6X5_Qrk6f-iadmgTO-muDLzhiH2Huf1uCMBq1ANY,21733
400
+ kailash-0.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
401
+ kailash-0.9.0.dist-info/entry_points.txt,sha256=M_q3b8PG5W4XbhSgESzIJjh3_4OBKtZFYFsOdkr2vO4,45
402
+ kailash-0.9.0.dist-info/top_level.txt,sha256=z7GzH2mxl66498pVf5HKwo5wwfPtt9Aq95uZUpH6JV0,8
403
+ kailash-0.9.0.dist-info/RECORD,,