kailash 0.1.4__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 +38 -0
- kailash/nodes/ai/a2a.py +1790 -0
- kailash/nodes/ai/agents.py +116 -2
- kailash/nodes/ai/ai_providers.py +206 -8
- kailash/nodes/ai/intelligent_agent_orchestrator.py +2108 -0
- kailash/nodes/ai/iterative_llm_agent.py +1280 -0
- kailash/nodes/ai/llm_agent.py +324 -1
- kailash/nodes/ai/self_organizing.py +1623 -0
- kailash/nodes/api/http.py +106 -25
- kailash/nodes/api/rest.py +116 -21
- 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 +116 -53
- kailash/nodes/data/writers.py +16 -6
- kailash/nodes/logic/__init__.py +8 -0
- kailash/nodes/logic/async_operations.py +48 -9
- kailash/nodes/logic/convergence.py +642 -0
- kailash/nodes/logic/loop.py +153 -0
- kailash/nodes/logic/operations.py +212 -27
- kailash/nodes/logic/workflow.py +26 -18
- kailash/nodes/mixins/__init__.py +11 -0
- kailash/nodes/mixins/mcp.py +228 -0
- kailash/nodes/mixins.py +387 -0
- kailash/nodes/transform/__init__.py +8 -1
- kailash/nodes/transform/processors.py +119 -4
- 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.4.dist-info → kailash-0.2.0.dist-info}/METADATA +446 -13
- 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.4.dist-info/RECORD +0 -85
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,365 @@
|
|
1
|
+
"""Cycle safety and resource management framework."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
import threading
|
5
|
+
import time
|
6
|
+
from contextlib import contextmanager
|
7
|
+
from typing import Dict, Optional, Set
|
8
|
+
|
9
|
+
import psutil
|
10
|
+
|
11
|
+
from kailash.sdk_exceptions import WorkflowExecutionError
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
class CycleSafetyManager:
|
17
|
+
"""Manages cycle execution safety and resource limits."""
|
18
|
+
|
19
|
+
def __init__(self):
|
20
|
+
"""Initialize cycle safety manager."""
|
21
|
+
self.active_cycles: Dict[str, "CycleMonitor"] = {}
|
22
|
+
self.global_memory_limit = None # MB
|
23
|
+
self.global_timeout = None # seconds
|
24
|
+
self._lock = threading.Lock()
|
25
|
+
|
26
|
+
def set_global_limits(
|
27
|
+
self, memory_limit: Optional[int] = None, timeout: Optional[float] = None
|
28
|
+
) -> None:
|
29
|
+
"""Set global resource limits.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
memory_limit: Global memory limit in MB
|
33
|
+
timeout: Global timeout in seconds
|
34
|
+
"""
|
35
|
+
self.global_memory_limit = memory_limit
|
36
|
+
self.global_timeout = timeout
|
37
|
+
|
38
|
+
def start_monitoring(
|
39
|
+
self,
|
40
|
+
cycle_id: str,
|
41
|
+
max_iterations: Optional[int] = None,
|
42
|
+
timeout: Optional[float] = None,
|
43
|
+
memory_limit: Optional[int] = None,
|
44
|
+
) -> "CycleMonitor":
|
45
|
+
"""Start monitoring a cycle.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
cycle_id: Cycle identifier
|
49
|
+
max_iterations: Maximum iterations allowed
|
50
|
+
timeout: Timeout in seconds
|
51
|
+
memory_limit: Memory limit in MB
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
CycleMonitor instance
|
55
|
+
"""
|
56
|
+
with self._lock:
|
57
|
+
if cycle_id in self.active_cycles:
|
58
|
+
logger.warning(f"Cycle {cycle_id} already being monitored")
|
59
|
+
return self.active_cycles[cycle_id]
|
60
|
+
|
61
|
+
# Use global limits if not specified
|
62
|
+
if timeout is None:
|
63
|
+
timeout = self.global_timeout
|
64
|
+
if memory_limit is None:
|
65
|
+
memory_limit = self.global_memory_limit
|
66
|
+
|
67
|
+
monitor = CycleMonitor(
|
68
|
+
cycle_id=cycle_id,
|
69
|
+
max_iterations=max_iterations,
|
70
|
+
timeout=timeout,
|
71
|
+
memory_limit=memory_limit,
|
72
|
+
)
|
73
|
+
|
74
|
+
self.active_cycles[cycle_id] = monitor
|
75
|
+
monitor.start()
|
76
|
+
|
77
|
+
return monitor
|
78
|
+
|
79
|
+
def stop_monitoring(self, cycle_id: str) -> None:
|
80
|
+
"""Stop monitoring a cycle.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
cycle_id: Cycle identifier
|
84
|
+
"""
|
85
|
+
with self._lock:
|
86
|
+
if cycle_id in self.active_cycles:
|
87
|
+
monitor = self.active_cycles[cycle_id]
|
88
|
+
monitor.stop()
|
89
|
+
del self.active_cycles[cycle_id]
|
90
|
+
|
91
|
+
def check_all_cycles(self) -> Dict[str, bool]:
|
92
|
+
"""Check all active cycles for violations.
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
Dict mapping cycle_id to violation status
|
96
|
+
"""
|
97
|
+
violations = {}
|
98
|
+
|
99
|
+
with self._lock:
|
100
|
+
for cycle_id, monitor in self.active_cycles.items():
|
101
|
+
violations[cycle_id] = monitor.check_violations()
|
102
|
+
|
103
|
+
return violations
|
104
|
+
|
105
|
+
def get_cycle_status(self, cycle_id: str) -> Optional[Dict[str, any]]:
|
106
|
+
"""Get status of a specific cycle.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
cycle_id: Cycle identifier
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
Status dict or None if not found
|
113
|
+
"""
|
114
|
+
with self._lock:
|
115
|
+
if cycle_id in self.active_cycles:
|
116
|
+
return self.active_cycles[cycle_id].get_status()
|
117
|
+
return None
|
118
|
+
|
119
|
+
def detect_deadlocks(self) -> Set[str]:
|
120
|
+
"""Detect potential deadlocks in active cycles.
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
Set of cycle IDs that may be deadlocked
|
124
|
+
"""
|
125
|
+
deadlocked = set()
|
126
|
+
|
127
|
+
with self._lock:
|
128
|
+
for cycle_id, monitor in self.active_cycles.items():
|
129
|
+
if monitor.is_potentially_deadlocked():
|
130
|
+
deadlocked.add(cycle_id)
|
131
|
+
|
132
|
+
return deadlocked
|
133
|
+
|
134
|
+
|
135
|
+
class CycleMonitor:
|
136
|
+
"""Monitors a single cycle for safety violations."""
|
137
|
+
|
138
|
+
def __init__(
|
139
|
+
self,
|
140
|
+
cycle_id: str,
|
141
|
+
max_iterations: Optional[int] = None,
|
142
|
+
timeout: Optional[float] = None,
|
143
|
+
memory_limit: Optional[int] = None,
|
144
|
+
):
|
145
|
+
"""Initialize cycle monitor.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
cycle_id: Cycle identifier
|
149
|
+
max_iterations: Maximum iterations allowed
|
150
|
+
timeout: Timeout in seconds
|
151
|
+
memory_limit: Memory limit in MB
|
152
|
+
"""
|
153
|
+
self.cycle_id = cycle_id
|
154
|
+
self.max_iterations = max_iterations or float("inf")
|
155
|
+
self.timeout = timeout
|
156
|
+
self.memory_limit = memory_limit
|
157
|
+
|
158
|
+
self.start_time = None
|
159
|
+
self.end_time = None
|
160
|
+
self.iteration_count = 0
|
161
|
+
self.last_progress_time = None
|
162
|
+
self.initial_memory = None
|
163
|
+
self.peak_memory = 0
|
164
|
+
self.violations = []
|
165
|
+
self.is_active = False
|
166
|
+
|
167
|
+
self._monitor_thread = None
|
168
|
+
self._stop_event = threading.Event()
|
169
|
+
|
170
|
+
def start(self) -> None:
|
171
|
+
"""Start monitoring."""
|
172
|
+
self.start_time = time.time()
|
173
|
+
self.last_progress_time = self.start_time
|
174
|
+
self.is_active = True
|
175
|
+
|
176
|
+
# Get initial memory usage
|
177
|
+
process = psutil.Process()
|
178
|
+
self.initial_memory = process.memory_info().rss / 1024 / 1024 # MB
|
179
|
+
|
180
|
+
# Start monitoring thread if we have limits
|
181
|
+
if self.timeout or self.memory_limit:
|
182
|
+
self._monitor_thread = threading.Thread(
|
183
|
+
target=self._monitor_loop, daemon=True
|
184
|
+
)
|
185
|
+
self._monitor_thread.start()
|
186
|
+
|
187
|
+
logger.info(f"Started monitoring cycle: {self.cycle_id}")
|
188
|
+
|
189
|
+
def stop(self) -> None:
|
190
|
+
"""Stop monitoring."""
|
191
|
+
self.end_time = time.time()
|
192
|
+
self.is_active = False
|
193
|
+
self._stop_event.set()
|
194
|
+
|
195
|
+
if self._monitor_thread:
|
196
|
+
self._monitor_thread.join(timeout=1)
|
197
|
+
|
198
|
+
logger.info(f"Stopped monitoring cycle: {self.cycle_id}")
|
199
|
+
|
200
|
+
def record_iteration(self) -> None:
|
201
|
+
"""Record that an iteration occurred."""
|
202
|
+
self.iteration_count += 1
|
203
|
+
self.last_progress_time = time.time()
|
204
|
+
|
205
|
+
# Check iteration limit
|
206
|
+
if self.iteration_count > self.max_iterations:
|
207
|
+
violation = f"Exceeded max iterations: {self.iteration_count} > {self.max_iterations}"
|
208
|
+
self.violations.append(violation)
|
209
|
+
raise WorkflowExecutionError(f"Cycle {self.cycle_id}: {violation}")
|
210
|
+
|
211
|
+
def check_violations(self) -> bool:
|
212
|
+
"""Check for any safety violations.
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
True if violations detected
|
216
|
+
"""
|
217
|
+
if not self.is_active:
|
218
|
+
return False
|
219
|
+
|
220
|
+
current_time = time.time()
|
221
|
+
|
222
|
+
# Check timeout
|
223
|
+
if self.timeout and (current_time - self.start_time) > self.timeout:
|
224
|
+
violation = f"Timeout exceeded: {current_time - self.start_time:.1f}s > {self.timeout}s"
|
225
|
+
self.violations.append(violation)
|
226
|
+
return True
|
227
|
+
|
228
|
+
# Check memory limit
|
229
|
+
if self.memory_limit:
|
230
|
+
process = psutil.Process()
|
231
|
+
current_memory = process.memory_info().rss / 1024 / 1024 # MB
|
232
|
+
memory_increase = current_memory - self.initial_memory
|
233
|
+
|
234
|
+
if memory_increase > self.memory_limit:
|
235
|
+
violation = f"Memory limit exceeded: {memory_increase:.1f}MB > {self.memory_limit}MB"
|
236
|
+
self.violations.append(violation)
|
237
|
+
return True
|
238
|
+
|
239
|
+
# Track peak memory
|
240
|
+
self.peak_memory = max(self.peak_memory, memory_increase)
|
241
|
+
|
242
|
+
return False
|
243
|
+
|
244
|
+
def is_potentially_deadlocked(self, stall_threshold: float = 60.0) -> bool:
|
245
|
+
"""Check if cycle might be deadlocked.
|
246
|
+
|
247
|
+
Args:
|
248
|
+
stall_threshold: Seconds without progress to consider deadlock
|
249
|
+
|
250
|
+
Returns:
|
251
|
+
True if potentially deadlocked
|
252
|
+
"""
|
253
|
+
if not self.is_active or not self.last_progress_time:
|
254
|
+
return False
|
255
|
+
|
256
|
+
time_since_progress = time.time() - self.last_progress_time
|
257
|
+
return time_since_progress > stall_threshold
|
258
|
+
|
259
|
+
def get_status(self) -> Dict[str, any]:
|
260
|
+
"""Get current monitor status.
|
261
|
+
|
262
|
+
Returns:
|
263
|
+
Status dictionary
|
264
|
+
"""
|
265
|
+
status = {
|
266
|
+
"cycle_id": self.cycle_id,
|
267
|
+
"is_active": self.is_active,
|
268
|
+
"iteration_count": self.iteration_count,
|
269
|
+
"elapsed_time": time.time() - self.start_time if self.start_time else 0,
|
270
|
+
"violations": self.violations,
|
271
|
+
}
|
272
|
+
|
273
|
+
if self.timeout:
|
274
|
+
status["timeout"] = self.timeout
|
275
|
+
status["time_remaining"] = max(0, self.timeout - status["elapsed_time"])
|
276
|
+
|
277
|
+
if self.memory_limit:
|
278
|
+
process = psutil.Process()
|
279
|
+
current_memory = process.memory_info().rss / 1024 / 1024 # MB
|
280
|
+
memory_increase = current_memory - self.initial_memory
|
281
|
+
|
282
|
+
status["memory_limit"] = self.memory_limit
|
283
|
+
status["memory_used"] = memory_increase
|
284
|
+
status["memory_remaining"] = max(0, self.memory_limit - memory_increase)
|
285
|
+
status["peak_memory"] = self.peak_memory
|
286
|
+
|
287
|
+
if self.max_iterations != float("inf"):
|
288
|
+
status["max_iterations"] = self.max_iterations
|
289
|
+
status["iterations_remaining"] = max(
|
290
|
+
0, self.max_iterations - self.iteration_count
|
291
|
+
)
|
292
|
+
|
293
|
+
return status
|
294
|
+
|
295
|
+
def _monitor_loop(self) -> None:
|
296
|
+
"""Background monitoring loop."""
|
297
|
+
check_interval = 1.0 # seconds
|
298
|
+
|
299
|
+
while not self._stop_event.is_set() and self.is_active:
|
300
|
+
try:
|
301
|
+
if self.check_violations():
|
302
|
+
logger.error(
|
303
|
+
f"Cycle {self.cycle_id} safety violation: {self.violations[-1]}"
|
304
|
+
)
|
305
|
+
# Could implement automatic termination here
|
306
|
+
|
307
|
+
self._stop_event.wait(check_interval)
|
308
|
+
|
309
|
+
except Exception as e:
|
310
|
+
logger.error(f"Error in monitor loop for cycle {self.cycle_id}: {e}")
|
311
|
+
|
312
|
+
|
313
|
+
@contextmanager
|
314
|
+
def monitored_cycle(safety_manager: CycleSafetyManager, cycle_id: str, **limits):
|
315
|
+
"""Context manager for monitored cycle execution.
|
316
|
+
|
317
|
+
Args:
|
318
|
+
safety_manager: CycleSafetyManager instance
|
319
|
+
cycle_id: Cycle identifier
|
320
|
+
**limits: Resource limits (max_iterations, timeout, memory_limit)
|
321
|
+
|
322
|
+
Yields:
|
323
|
+
CycleMonitor instance
|
324
|
+
"""
|
325
|
+
monitor = safety_manager.start_monitoring(cycle_id, **limits)
|
326
|
+
|
327
|
+
try:
|
328
|
+
yield monitor
|
329
|
+
finally:
|
330
|
+
safety_manager.stop_monitoring(cycle_id)
|
331
|
+
|
332
|
+
|
333
|
+
class TimeoutHandler:
|
334
|
+
"""Handles timeout for cycle execution."""
|
335
|
+
|
336
|
+
def __init__(self, timeout: float):
|
337
|
+
"""Initialize timeout handler.
|
338
|
+
|
339
|
+
Args:
|
340
|
+
timeout: Timeout in seconds
|
341
|
+
"""
|
342
|
+
self.timeout = timeout
|
343
|
+
self.timer = None
|
344
|
+
self.timed_out = False
|
345
|
+
|
346
|
+
def __enter__(self):
|
347
|
+
"""Start timeout timer."""
|
348
|
+
|
349
|
+
def timeout_handler():
|
350
|
+
self.timed_out = True
|
351
|
+
logger.error(f"Cycle execution timed out after {self.timeout}s")
|
352
|
+
|
353
|
+
self.timer = threading.Timer(self.timeout, timeout_handler)
|
354
|
+
self.timer.start()
|
355
|
+
return self
|
356
|
+
|
357
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
358
|
+
"""Cancel timeout timer."""
|
359
|
+
if self.timer:
|
360
|
+
self.timer.cancel()
|
361
|
+
|
362
|
+
if self.timed_out and exc_type is None:
|
363
|
+
raise WorkflowExecutionError(
|
364
|
+
f"Cycle execution timed out after {self.timeout}s"
|
365
|
+
)
|