kailash 0.1.5__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kailash/__init__.py +1 -1
- kailash/access_control.py +740 -0
- kailash/api/__main__.py +6 -0
- kailash/api/auth.py +668 -0
- kailash/api/custom_nodes.py +285 -0
- kailash/api/custom_nodes_secure.py +377 -0
- kailash/api/database.py +620 -0
- kailash/api/studio.py +915 -0
- kailash/api/studio_secure.py +893 -0
- kailash/mcp/__init__.py +53 -0
- kailash/mcp/__main__.py +13 -0
- kailash/mcp/ai_registry_server.py +712 -0
- kailash/mcp/client.py +447 -0
- kailash/mcp/client_new.py +334 -0
- kailash/mcp/server.py +293 -0
- kailash/mcp/server_new.py +336 -0
- kailash/mcp/servers/__init__.py +12 -0
- kailash/mcp/servers/ai_registry.py +289 -0
- kailash/nodes/__init__.py +4 -2
- kailash/nodes/ai/__init__.py +2 -0
- kailash/nodes/ai/a2a.py +714 -67
- kailash/nodes/ai/intelligent_agent_orchestrator.py +31 -37
- kailash/nodes/ai/iterative_llm_agent.py +1280 -0
- kailash/nodes/ai/llm_agent.py +324 -1
- kailash/nodes/ai/self_organizing.py +5 -6
- kailash/nodes/base.py +15 -2
- kailash/nodes/base_async.py +45 -0
- kailash/nodes/base_cycle_aware.py +374 -0
- kailash/nodes/base_with_acl.py +338 -0
- kailash/nodes/code/python.py +135 -27
- kailash/nodes/data/readers.py +16 -6
- kailash/nodes/data/writers.py +16 -6
- kailash/nodes/logic/__init__.py +8 -0
- kailash/nodes/logic/convergence.py +642 -0
- kailash/nodes/logic/loop.py +153 -0
- kailash/nodes/logic/operations.py +187 -27
- kailash/nodes/mixins/__init__.py +11 -0
- kailash/nodes/mixins/mcp.py +228 -0
- kailash/nodes/mixins.py +387 -0
- kailash/runtime/__init__.py +2 -1
- kailash/runtime/access_controlled.py +458 -0
- kailash/runtime/local.py +106 -33
- kailash/runtime/parallel_cyclic.py +529 -0
- kailash/sdk_exceptions.py +90 -5
- kailash/security.py +845 -0
- kailash/tracking/manager.py +38 -15
- kailash/tracking/models.py +1 -1
- kailash/tracking/storage/filesystem.py +30 -2
- kailash/utils/__init__.py +8 -0
- kailash/workflow/__init__.py +18 -0
- kailash/workflow/convergence.py +270 -0
- kailash/workflow/cycle_analyzer.py +768 -0
- kailash/workflow/cycle_builder.py +573 -0
- kailash/workflow/cycle_config.py +709 -0
- kailash/workflow/cycle_debugger.py +760 -0
- kailash/workflow/cycle_exceptions.py +601 -0
- kailash/workflow/cycle_profiler.py +671 -0
- kailash/workflow/cycle_state.py +338 -0
- kailash/workflow/cyclic_runner.py +985 -0
- kailash/workflow/graph.py +500 -39
- kailash/workflow/migration.py +768 -0
- kailash/workflow/safety.py +365 -0
- kailash/workflow/templates.py +744 -0
- kailash/workflow/validation.py +693 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/METADATA +256 -12
- kailash-0.2.0.dist-info/RECORD +125 -0
- kailash/nodes/mcp/__init__.py +0 -11
- kailash/nodes/mcp/client.py +0 -554
- kailash/nodes/mcp/resource.py +0 -682
- kailash/nodes/mcp/server.py +0 -577
- kailash-0.1.5.dist-info/RECORD +0 -88
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,573 @@
|
|
1
|
+
"""
|
2
|
+
Fluent API for Cyclic Workflow Creation.
|
3
|
+
|
4
|
+
This module provides an intuitive, chainable API for creating cyclic workflows
|
5
|
+
with enhanced developer experience and IDE support. The CycleBuilder pattern
|
6
|
+
makes cycle configuration more discoverable and type-safe while maintaining
|
7
|
+
full backward compatibility with existing workflow construction methods.
|
8
|
+
|
9
|
+
Examples:
|
10
|
+
Basic cycle creation:
|
11
|
+
|
12
|
+
>>> workflow = Workflow("optimization", "Optimization Loop")
|
13
|
+
>>> cycle = workflow.create_cycle("quality_improvement")
|
14
|
+
>>> cycle.connect("processor", "evaluator") \
|
15
|
+
... .max_iterations(50) \
|
16
|
+
... .converge_when("quality > 0.9") \
|
17
|
+
... .timeout(300) \
|
18
|
+
... .build()
|
19
|
+
|
20
|
+
Advanced configuration:
|
21
|
+
|
22
|
+
>>> cycle = workflow.create_cycle("complex_optimization") \
|
23
|
+
... .connect("optimizer", "evaluator", {"result": "input_data"}) \
|
24
|
+
... .max_iterations(100) \
|
25
|
+
... .converge_when("improvement < 0.001") \
|
26
|
+
... .timeout(600) \
|
27
|
+
... .memory_limit(2048) \
|
28
|
+
... .when("needs_optimization == True") \
|
29
|
+
... .nested_in("outer_cycle") \
|
30
|
+
... .build()
|
31
|
+
|
32
|
+
Template integration:
|
33
|
+
|
34
|
+
>>> from kailash.workflow.cycle_config import CycleTemplates
|
35
|
+
>>> config = CycleTemplates.optimization_loop(max_iterations=50)
|
36
|
+
>>> cycle = CycleBuilder.from_config(workflow, config) \
|
37
|
+
... .connect("processor", "evaluator") \
|
38
|
+
... .timeout(120) # Override template value
|
39
|
+
... .build()
|
40
|
+
"""
|
41
|
+
|
42
|
+
import logging
|
43
|
+
from typing import Dict, Optional, TYPE_CHECKING
|
44
|
+
|
45
|
+
from kailash.sdk_exceptions import WorkflowValidationError
|
46
|
+
from kailash.workflow.cycle_exceptions import (
|
47
|
+
CycleConfigurationError,
|
48
|
+
CycleConnectionError
|
49
|
+
)
|
50
|
+
|
51
|
+
if TYPE_CHECKING:
|
52
|
+
from .graph import Workflow
|
53
|
+
from .cycle_config import CycleConfig
|
54
|
+
|
55
|
+
logger = logging.getLogger(__name__)
|
56
|
+
|
57
|
+
|
58
|
+
class CycleBuilder:
|
59
|
+
"""
|
60
|
+
Fluent builder for creating cyclic workflow connections.
|
61
|
+
|
62
|
+
This class provides an intuitive, chainable API for configuring cyclic
|
63
|
+
connections in workflows. It replaces the verbose parameter-heavy approach
|
64
|
+
with a more discoverable and type-safe builder pattern.
|
65
|
+
|
66
|
+
Examples:
|
67
|
+
Creating a basic cycle:
|
68
|
+
|
69
|
+
>>> workflow = Workflow("optimization", "Optimization Loop")
|
70
|
+
>>> cycle = workflow.create_cycle("quality_improvement")
|
71
|
+
>>> cycle.connect("processor", "evaluator") \
|
72
|
+
... .max_iterations(50) \
|
73
|
+
... .converge_when("quality > 0.9") \
|
74
|
+
... .timeout(300) \
|
75
|
+
... .build()
|
76
|
+
"""
|
77
|
+
|
78
|
+
def __init__(self, workflow: "Workflow", cycle_id: Optional[str] = None):
|
79
|
+
"""
|
80
|
+
Initialize a new CycleBuilder.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
workflow: The workflow to add the cycle to.
|
84
|
+
cycle_id: Optional identifier for the cycle group.
|
85
|
+
"""
|
86
|
+
self._workflow = workflow
|
87
|
+
self._cycle_id = cycle_id
|
88
|
+
|
89
|
+
# Connection parameters
|
90
|
+
self._source_node: Optional[str] = None
|
91
|
+
self._target_node: Optional[str] = None
|
92
|
+
self._mapping: Optional[Dict[str, str]] = None
|
93
|
+
|
94
|
+
# Cycle parameters
|
95
|
+
self._max_iterations: Optional[int] = None
|
96
|
+
self._convergence_check: Optional[str] = None
|
97
|
+
self._timeout: Optional[float] = None
|
98
|
+
self._memory_limit: Optional[int] = None
|
99
|
+
self._condition: Optional[str] = None
|
100
|
+
self._parent_cycle: Optional[str] = None
|
101
|
+
|
102
|
+
def connect(self, source_node: str, target_node: str,
|
103
|
+
mapping: Optional[Dict[str, str]] = None) -> "CycleBuilder":
|
104
|
+
"""
|
105
|
+
Configure the source and target nodes for the cycle connection.
|
106
|
+
|
107
|
+
Establishes which nodes will be connected in a cyclic pattern. The
|
108
|
+
mapping parameter defines how outputs from the source node map to
|
109
|
+
inputs of the target node.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
source_node: Node ID that produces output for the cycle.
|
113
|
+
target_node: Node ID that receives input from the cycle.
|
114
|
+
mapping: Output-to-input mapping. Keys are source output fields,
|
115
|
+
values are target input fields. If None, attempts automatic
|
116
|
+
mapping based on parameter names.
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
Self for method chaining.
|
120
|
+
|
121
|
+
Raises:
|
122
|
+
WorkflowValidationError: If source or target node doesn't exist.
|
123
|
+
CycleConfigurationError: If nodes are invalid for cyclic connection.
|
124
|
+
|
125
|
+
Examples:
|
126
|
+
>>> cycle.connect("processor", "evaluator", {"result": "input_data"})
|
127
|
+
>>> # Or with automatic mapping
|
128
|
+
>>> cycle.connect("node_a", "node_b")
|
129
|
+
"""
|
130
|
+
# Validate nodes exist in workflow
|
131
|
+
available_nodes = list(self._workflow.nodes.keys())
|
132
|
+
|
133
|
+
if source_node not in self._workflow.nodes:
|
134
|
+
raise CycleConnectionError(
|
135
|
+
f"Source node '{source_node}' not found in workflow",
|
136
|
+
source_node=source_node,
|
137
|
+
available_nodes=available_nodes,
|
138
|
+
error_code="CYCLE_CONN_001"
|
139
|
+
)
|
140
|
+
|
141
|
+
if target_node not in self._workflow.nodes:
|
142
|
+
raise CycleConnectionError(
|
143
|
+
f"Target node '{target_node}' not found in workflow",
|
144
|
+
target_node=target_node,
|
145
|
+
available_nodes=available_nodes,
|
146
|
+
error_code="CYCLE_CONN_002"
|
147
|
+
)
|
148
|
+
|
149
|
+
self._source_node = source_node
|
150
|
+
self._target_node = target_node
|
151
|
+
self._mapping = mapping
|
152
|
+
|
153
|
+
return self
|
154
|
+
|
155
|
+
def max_iterations(self, iterations: int) -> "CycleBuilder":
|
156
|
+
"""
|
157
|
+
Set the maximum number of cycle iterations for safety.
|
158
|
+
|
159
|
+
Provides a hard limit on cycle execution to prevent infinite loops.
|
160
|
+
This is a critical safety mechanism for production workflows.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
iterations: Maximum number of iterations allowed. Must be positive.
|
164
|
+
Recommended range: 10-1000 depending on use case.
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
Self for method chaining.
|
168
|
+
|
169
|
+
Raises:
|
170
|
+
CycleConfigurationError: If iterations is not positive.
|
171
|
+
|
172
|
+
Examples:
|
173
|
+
>>> cycle.max_iterations(100) # Allow up to 100 iterations
|
174
|
+
>>> cycle.max_iterations(10) # Quick convergence expected
|
175
|
+
"""
|
176
|
+
if iterations <= 0:
|
177
|
+
raise CycleConfigurationError(
|
178
|
+
"max_iterations must be positive",
|
179
|
+
cycle_id=self._cycle_id,
|
180
|
+
invalid_params={"max_iterations": iterations},
|
181
|
+
error_code="CYCLE_CONFIG_002",
|
182
|
+
suggestions=[
|
183
|
+
"Use 10-100 iterations for quick convergence",
|
184
|
+
"Use 100-1000 iterations for complex optimization",
|
185
|
+
"Consider adding convergence_check for early termination"
|
186
|
+
]
|
187
|
+
)
|
188
|
+
|
189
|
+
self._max_iterations = iterations
|
190
|
+
return self
|
191
|
+
|
192
|
+
def converge_when(self, condition: str) -> "CycleBuilder":
|
193
|
+
"""
|
194
|
+
Set the convergence condition to terminate the cycle early.
|
195
|
+
|
196
|
+
Defines an expression that, when true, will stop cycle execution
|
197
|
+
before reaching max_iterations. This enables efficient early
|
198
|
+
termination when the desired result is achieved.
|
199
|
+
|
200
|
+
Args:
|
201
|
+
condition: Python expression evaluated against node outputs.
|
202
|
+
Can reference any output field from cycle nodes.
|
203
|
+
Examples: "error < 0.01", "quality > 0.9", "improvement < 0.001"
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
Self for method chaining.
|
207
|
+
|
208
|
+
Raises:
|
209
|
+
CycleConfigurationError: If condition syntax is invalid.
|
210
|
+
|
211
|
+
Examples:
|
212
|
+
>>> cycle.converge_when("error < 0.01") # Numerical convergence
|
213
|
+
>>> cycle.converge_when("quality > 0.95") # Quality threshold
|
214
|
+
>>> cycle.converge_when("improvement < 0.001") # Minimal improvement
|
215
|
+
"""
|
216
|
+
if not condition or not isinstance(condition, str):
|
217
|
+
raise CycleConfigurationError(
|
218
|
+
"Convergence condition must be a non-empty string expression. "
|
219
|
+
"Examples: 'error < 0.01', 'quality > 0.9', 'count >= 10'"
|
220
|
+
)
|
221
|
+
|
222
|
+
# Basic validation - check for dangerous operations
|
223
|
+
dangerous_patterns = ['import ', 'exec(', 'eval(', '__']
|
224
|
+
for pattern in dangerous_patterns:
|
225
|
+
if pattern in condition:
|
226
|
+
raise CycleConfigurationError(
|
227
|
+
f"Convergence condition contains potentially unsafe operation: '{pattern}'. "
|
228
|
+
"Use simple comparison expressions only."
|
229
|
+
)
|
230
|
+
|
231
|
+
self._convergence_check = condition
|
232
|
+
return self
|
233
|
+
|
234
|
+
def timeout(self, seconds: float) -> "CycleBuilder":
|
235
|
+
"""
|
236
|
+
Set a timeout limit for cycle execution.
|
237
|
+
|
238
|
+
Provides time-based safety limit to prevent cycles from running
|
239
|
+
indefinitely. Useful for cycles that might have unpredictable
|
240
|
+
convergence times.
|
241
|
+
|
242
|
+
Args:
|
243
|
+
seconds: Maximum execution time in seconds. Must be positive.
|
244
|
+
Recommended: 30-3600 seconds.
|
245
|
+
|
246
|
+
Returns:
|
247
|
+
Self for method chaining.
|
248
|
+
|
249
|
+
Raises:
|
250
|
+
CycleConfigurationError: If timeout is not positive.
|
251
|
+
|
252
|
+
Examples:
|
253
|
+
>>> cycle.timeout(300) # 5 minutes maximum
|
254
|
+
>>> cycle.timeout(30.5) # 30.5 seconds for quick cycles
|
255
|
+
"""
|
256
|
+
if seconds <= 0:
|
257
|
+
raise CycleConfigurationError(
|
258
|
+
f"Timeout must be positive, got {seconds}. "
|
259
|
+
"Recommendation: Use 30-300 seconds for most cycles, "
|
260
|
+
"up to 3600 seconds for long-running optimization."
|
261
|
+
)
|
262
|
+
|
263
|
+
self._timeout = seconds
|
264
|
+
return self
|
265
|
+
|
266
|
+
def memory_limit(self, mb: int) -> "CycleBuilder":
|
267
|
+
"""
|
268
|
+
Set a memory usage limit for cycle execution.
|
269
|
+
|
270
|
+
Provides memory-based safety limit to prevent cycles from consuming
|
271
|
+
excessive memory through data accumulation across iterations.
|
272
|
+
|
273
|
+
Args:
|
274
|
+
mb: Maximum memory usage in megabytes. Must be positive.
|
275
|
+
Recommended: 100-10000 MB.
|
276
|
+
|
277
|
+
Returns:
|
278
|
+
Self for method chaining.
|
279
|
+
|
280
|
+
Raises:
|
281
|
+
CycleConfigurationError: If memory limit is not positive.
|
282
|
+
|
283
|
+
Examples:
|
284
|
+
>>> cycle.memory_limit(1024) # 1GB limit
|
285
|
+
>>> cycle.memory_limit(512) # 512MB for smaller workflows
|
286
|
+
"""
|
287
|
+
if mb <= 0:
|
288
|
+
raise CycleConfigurationError(
|
289
|
+
f"Memory limit must be positive, got {mb}. "
|
290
|
+
"Recommendation: Use 100-1000 MB for most cycles, "
|
291
|
+
"up to 10000 MB for data-intensive processing."
|
292
|
+
)
|
293
|
+
|
294
|
+
self._memory_limit = mb
|
295
|
+
return self
|
296
|
+
|
297
|
+
def when(self, condition: str) -> "CycleBuilder":
|
298
|
+
"""
|
299
|
+
Set a conditional expression for cycle routing.
|
300
|
+
|
301
|
+
Enables conditional cycle execution where the cycle only runs
|
302
|
+
when the specified condition is met. Useful for adaptive workflows.
|
303
|
+
|
304
|
+
Args:
|
305
|
+
condition: Python expression for conditional execution.
|
306
|
+
Evaluated before each cycle iteration.
|
307
|
+
|
308
|
+
Returns:
|
309
|
+
Self for method chaining.
|
310
|
+
|
311
|
+
Raises:
|
312
|
+
CycleConfigurationError: If condition syntax is invalid.
|
313
|
+
|
314
|
+
Examples:
|
315
|
+
>>> cycle.when("retry_count < 3") # Retry logic
|
316
|
+
>>> cycle.when("needs_optimization") # Conditional optimization
|
317
|
+
"""
|
318
|
+
if not condition or not isinstance(condition, str):
|
319
|
+
raise CycleConfigurationError(
|
320
|
+
"Condition must be a non-empty string expression. "
|
321
|
+
"Examples: 'retry_count < 3', 'needs_improvement == True'"
|
322
|
+
)
|
323
|
+
|
324
|
+
self._condition = condition
|
325
|
+
return self
|
326
|
+
|
327
|
+
def nested_in(self, parent_cycle_id: str) -> "CycleBuilder":
|
328
|
+
"""
|
329
|
+
Make this cycle nested within another cycle.
|
330
|
+
|
331
|
+
Enables hierarchical cycle structures where one cycle operates
|
332
|
+
within the iterations of a parent cycle. Useful for multi-level
|
333
|
+
optimization scenarios.
|
334
|
+
|
335
|
+
Args:
|
336
|
+
parent_cycle_id: Identifier of the parent cycle.
|
337
|
+
|
338
|
+
Returns:
|
339
|
+
Self for method chaining.
|
340
|
+
|
341
|
+
Raises:
|
342
|
+
CycleConfigurationError: If parent cycle ID is invalid.
|
343
|
+
|
344
|
+
Examples:
|
345
|
+
>>> cycle.nested_in("outer_optimization") # This cycle runs inside outer_optimization
|
346
|
+
"""
|
347
|
+
if not parent_cycle_id or not isinstance(parent_cycle_id, str):
|
348
|
+
raise CycleConfigurationError(
|
349
|
+
"Parent cycle ID must be a non-empty string"
|
350
|
+
)
|
351
|
+
|
352
|
+
self._parent_cycle = parent_cycle_id
|
353
|
+
return self
|
354
|
+
|
355
|
+
def build(self) -> None:
|
356
|
+
"""
|
357
|
+
Build and add the configured cycle to the workflow.
|
358
|
+
|
359
|
+
Validates the cycle configuration and creates the actual cyclic
|
360
|
+
connection in the workflow. This finalizes the cycle builder
|
361
|
+
pattern and applies all configured settings.
|
362
|
+
|
363
|
+
Raises:
|
364
|
+
CycleConfigurationError: If cycle configuration is incomplete or invalid.
|
365
|
+
WorkflowValidationError: If workflow connection fails.
|
366
|
+
|
367
|
+
Examples:
|
368
|
+
>>> cycle.connect("node_a", "node_b") \
|
369
|
+
... .max_iterations(50) \
|
370
|
+
... .converge_when("quality > 0.9") \
|
371
|
+
... .build() # Creates the cycle in the workflow
|
372
|
+
"""
|
373
|
+
# Validate required parameters
|
374
|
+
if not self._source_node or not self._target_node:
|
375
|
+
raise CycleConfigurationError(
|
376
|
+
"Cycle must have source and target nodes configured. "
|
377
|
+
"Call connect(source_node, target_node) before build()."
|
378
|
+
)
|
379
|
+
|
380
|
+
# Validate at least one termination condition
|
381
|
+
if not self._max_iterations and not self._convergence_check and not self._timeout:
|
382
|
+
raise CycleConfigurationError(
|
383
|
+
"Cycle must have at least one termination condition. "
|
384
|
+
"Add max_iterations(), converge_when(), or timeout() before build(). "
|
385
|
+
"Recommendation: Always include max_iterations() as a safety net."
|
386
|
+
)
|
387
|
+
|
388
|
+
# Create the connection using the workflow's connect method
|
389
|
+
try:
|
390
|
+
self._workflow.connect(
|
391
|
+
source_node=self._source_node,
|
392
|
+
target_node=self._target_node,
|
393
|
+
mapping=self._mapping,
|
394
|
+
cycle=True,
|
395
|
+
max_iterations=self._max_iterations,
|
396
|
+
convergence_check=self._convergence_check,
|
397
|
+
cycle_id=self._cycle_id,
|
398
|
+
timeout=self._timeout,
|
399
|
+
memory_limit=self._memory_limit,
|
400
|
+
condition=self._condition,
|
401
|
+
parent_cycle=self._parent_cycle
|
402
|
+
)
|
403
|
+
|
404
|
+
logger.info(
|
405
|
+
f"Created cycle '{self._cycle_id or 'unnamed'}' from "
|
406
|
+
f"{self._source_node} to {self._target_node} with "
|
407
|
+
f"max_iterations={self._max_iterations}, "
|
408
|
+
f"convergence='{self._convergence_check}'"
|
409
|
+
)
|
410
|
+
|
411
|
+
except Exception as e:
|
412
|
+
raise WorkflowValidationError(
|
413
|
+
f"Failed to create cycle connection: {e}"
|
414
|
+
) from e
|
415
|
+
|
416
|
+
@classmethod
|
417
|
+
def from_config(cls, workflow: "Workflow", config: "CycleConfig") -> "CycleBuilder":
|
418
|
+
"""
|
419
|
+
Create a CycleBuilder from a CycleConfig instance.
|
420
|
+
|
421
|
+
Provides an alternative constructor that initializes the builder
|
422
|
+
with all configuration from a type-safe CycleConfig object. This
|
423
|
+
enables configuration reuse, templating, and structured configuration
|
424
|
+
management across multiple cycles.
|
425
|
+
|
426
|
+
Args:
|
427
|
+
workflow: Target workflow for the cycle.
|
428
|
+
config: Pre-configured cycle parameters.
|
429
|
+
|
430
|
+
Returns:
|
431
|
+
Builder instance initialized with config values.
|
432
|
+
|
433
|
+
Raises:
|
434
|
+
CycleConfigurationError: If config is invalid.
|
435
|
+
ImportError: If CycleConfig module is not available.
|
436
|
+
|
437
|
+
Examples:
|
438
|
+
Using a template:
|
439
|
+
|
440
|
+
>>> config = CycleTemplates.optimization_loop(max_iterations=50)
|
441
|
+
>>> builder = CycleBuilder.from_config(workflow, config)
|
442
|
+
>>> builder.connect("optimizer", "evaluator").build()
|
443
|
+
|
444
|
+
Using custom configuration:
|
445
|
+
|
446
|
+
>>> config = CycleConfig(max_iterations=100, timeout=300)
|
447
|
+
>>> builder = CycleBuilder.from_config(workflow, config)
|
448
|
+
>>> builder.connect("processor", "evaluator").build()
|
449
|
+
"""
|
450
|
+
try:
|
451
|
+
from kailash.workflow.cycle_config import CycleConfig
|
452
|
+
except ImportError as e:
|
453
|
+
raise ImportError(
|
454
|
+
"CycleConfig not available. Ensure kailash.workflow.cycle_config is installed."
|
455
|
+
) from e
|
456
|
+
|
457
|
+
# Validate config is correct type
|
458
|
+
if not isinstance(config, CycleConfig):
|
459
|
+
raise CycleConfigurationError(
|
460
|
+
f"Expected CycleConfig instance, got {type(config)}. "
|
461
|
+
"Use CycleConfig() or CycleTemplates.* to create valid configuration."
|
462
|
+
)
|
463
|
+
|
464
|
+
# Create builder with config values
|
465
|
+
builder = cls(workflow=workflow, cycle_id=config.cycle_id)
|
466
|
+
|
467
|
+
# Apply configuration parameters
|
468
|
+
if config.max_iterations is not None:
|
469
|
+
builder._max_iterations = config.max_iterations
|
470
|
+
|
471
|
+
if config.convergence_check is not None:
|
472
|
+
if isinstance(config.convergence_check, str):
|
473
|
+
builder._convergence_check = config.convergence_check
|
474
|
+
else:
|
475
|
+
# For callable convergence checks, convert to description
|
476
|
+
builder._convergence_check = "<callable_convergence_check>"
|
477
|
+
|
478
|
+
if config.timeout is not None:
|
479
|
+
builder._timeout = config.timeout
|
480
|
+
|
481
|
+
if config.memory_limit is not None:
|
482
|
+
builder._memory_limit = config.memory_limit
|
483
|
+
|
484
|
+
if config.condition is not None:
|
485
|
+
builder._condition = config.condition
|
486
|
+
|
487
|
+
if config.parent_cycle is not None:
|
488
|
+
builder._parent_cycle = config.parent_cycle
|
489
|
+
|
490
|
+
return builder
|
491
|
+
|
492
|
+
def apply_config(self, config: "CycleConfig") -> "CycleBuilder":
|
493
|
+
"""
|
494
|
+
Apply configuration from a CycleConfig instance to this builder.
|
495
|
+
|
496
|
+
Merges configuration parameters from a CycleConfig object into
|
497
|
+
the current builder state. This allows combining fluent builder
|
498
|
+
calls with structured configuration objects for maximum flexibility.
|
499
|
+
|
500
|
+
Args:
|
501
|
+
config: Configuration to apply to this builder.
|
502
|
+
|
503
|
+
Returns:
|
504
|
+
Self for method chaining.
|
505
|
+
|
506
|
+
Raises:
|
507
|
+
CycleConfigurationError: If config is invalid.
|
508
|
+
|
509
|
+
Examples:
|
510
|
+
>>> builder = workflow.create_cycle("custom") \
|
511
|
+
... .connect("a", "b") \
|
512
|
+
... .apply_config(CycleTemplates.optimization_loop()) \
|
513
|
+
... .timeout(120) # Override the template timeout
|
514
|
+
... .build()
|
515
|
+
"""
|
516
|
+
try:
|
517
|
+
from kailash.workflow.cycle_config import CycleConfig
|
518
|
+
except ImportError as e:
|
519
|
+
raise ImportError(
|
520
|
+
"CycleConfig not available. Ensure kailash.workflow.cycle_config is installed."
|
521
|
+
) from e
|
522
|
+
|
523
|
+
if not isinstance(config, CycleConfig):
|
524
|
+
raise CycleConfigurationError(
|
525
|
+
f"Expected CycleConfig instance, got {type(config)}"
|
526
|
+
)
|
527
|
+
|
528
|
+
# Apply non-None configuration values
|
529
|
+
if config.max_iterations is not None:
|
530
|
+
self._max_iterations = config.max_iterations
|
531
|
+
|
532
|
+
if config.convergence_check is not None:
|
533
|
+
if isinstance(config.convergence_check, str):
|
534
|
+
self._convergence_check = config.convergence_check
|
535
|
+
|
536
|
+
if config.timeout is not None:
|
537
|
+
self._timeout = config.timeout
|
538
|
+
|
539
|
+
if config.memory_limit is not None:
|
540
|
+
self._memory_limit = config.memory_limit
|
541
|
+
|
542
|
+
if config.condition is not None:
|
543
|
+
self._condition = config.condition
|
544
|
+
|
545
|
+
if config.parent_cycle is not None:
|
546
|
+
self._parent_cycle = config.parent_cycle
|
547
|
+
|
548
|
+
# Update cycle_id if specified in config
|
549
|
+
if config.cycle_id is not None:
|
550
|
+
self._cycle_id = config.cycle_id
|
551
|
+
|
552
|
+
return self
|
553
|
+
|
554
|
+
def __repr__(self) -> str:
|
555
|
+
"""
|
556
|
+
Return string representation of the cycle builder configuration.
|
557
|
+
|
558
|
+
Returns:
|
559
|
+
Human-readable representation of current configuration.
|
560
|
+
|
561
|
+
Examples:
|
562
|
+
>>> str(cycle)
|
563
|
+
'CycleBuilder(cycle_id=optimization, source=processor, target=evaluator, max_iterations=50)'
|
564
|
+
"""
|
565
|
+
return (
|
566
|
+
f"CycleBuilder("
|
567
|
+
f"cycle_id={self._cycle_id}, "
|
568
|
+
f"source={self._source_node}, "
|
569
|
+
f"target={self._target_node}, "
|
570
|
+
f"max_iterations={self._max_iterations}, "
|
571
|
+
f"convergence='{self._convergence_check}'"
|
572
|
+
f")"
|
573
|
+
)
|