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,338 @@
|
|
1
|
+
"""Cycle state management for workflow iterations."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
import time
|
5
|
+
from datetime import datetime, timezone
|
6
|
+
from typing import Any, Dict, List, Optional
|
7
|
+
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
10
|
+
|
11
|
+
class CycleState:
|
12
|
+
"""Manages state across cycle iterations."""
|
13
|
+
|
14
|
+
def __init__(self, cycle_id: str = "default"):
|
15
|
+
"""Initialize cycle state.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
cycle_id: Identifier for this cycle group
|
19
|
+
"""
|
20
|
+
self.cycle_id = cycle_id
|
21
|
+
self.iteration = 0
|
22
|
+
self.history: List[Dict[str, Any]] = []
|
23
|
+
self.metadata: Dict[str, Any] = {}
|
24
|
+
self.start_time = time.time()
|
25
|
+
self.last_update_time = self.start_time
|
26
|
+
self.node_states: Dict[str, Any] = {} # Per-node state storage
|
27
|
+
|
28
|
+
@property
|
29
|
+
def elapsed_time(self) -> float:
|
30
|
+
"""Get elapsed time since cycle started."""
|
31
|
+
return time.time() - self.start_time
|
32
|
+
|
33
|
+
@property
|
34
|
+
def iteration_time(self) -> float:
|
35
|
+
"""Get time since last iteration."""
|
36
|
+
return time.time() - self.last_update_time
|
37
|
+
|
38
|
+
def update(self, results: Dict[str, Any], iteration: Optional[int] = None) -> None:
|
39
|
+
"""Update state with iteration results.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
results: Results from current iteration
|
43
|
+
iteration: Optional iteration number (auto-incremented if not provided)
|
44
|
+
"""
|
45
|
+
if iteration is not None:
|
46
|
+
self.iteration = iteration
|
47
|
+
else:
|
48
|
+
self.iteration += 1
|
49
|
+
|
50
|
+
# Record iteration history
|
51
|
+
history_entry = {
|
52
|
+
"iteration": self.iteration,
|
53
|
+
"results": results,
|
54
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
55
|
+
"elapsed_time": self.elapsed_time,
|
56
|
+
"iteration_time": self.iteration_time,
|
57
|
+
}
|
58
|
+
self.history.append(history_entry)
|
59
|
+
|
60
|
+
# Update timing
|
61
|
+
self.last_update_time = time.time()
|
62
|
+
|
63
|
+
# Update node states if provided
|
64
|
+
for node_id, node_result in results.items():
|
65
|
+
if isinstance(node_result, dict) and "_cycle_state" in node_result:
|
66
|
+
self.node_states[node_id] = node_result["_cycle_state"]
|
67
|
+
|
68
|
+
logger.debug(
|
69
|
+
f"Cycle {self.cycle_id} updated: iteration={self.iteration}, "
|
70
|
+
f"elapsed_time={self.elapsed_time:.2f}s"
|
71
|
+
)
|
72
|
+
|
73
|
+
def get_node_state(self, node_id: str) -> Any:
|
74
|
+
"""Get state for specific node.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
node_id: Node identifier
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
Node state or None if not found
|
81
|
+
"""
|
82
|
+
return self.node_states.get(node_id)
|
83
|
+
|
84
|
+
def set_node_state(self, node_id: str, state: Any) -> None:
|
85
|
+
"""Set state for specific node.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
node_id: Node identifier
|
89
|
+
state: State to store
|
90
|
+
"""
|
91
|
+
self.node_states[node_id] = state
|
92
|
+
|
93
|
+
def get_convergence_context(self) -> Dict[str, Any]:
|
94
|
+
"""Get context for convergence evaluation.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
Dict with iteration info, history, and trends
|
98
|
+
"""
|
99
|
+
context = {
|
100
|
+
"iteration": self.iteration,
|
101
|
+
"history": self.history,
|
102
|
+
"elapsed_time": self.elapsed_time,
|
103
|
+
"metadata": self.metadata,
|
104
|
+
"node_states": self.node_states,
|
105
|
+
}
|
106
|
+
|
107
|
+
# Add trend analysis if we have history
|
108
|
+
if len(self.history) >= 2:
|
109
|
+
context["trend"] = self.calculate_trend()
|
110
|
+
|
111
|
+
return context
|
112
|
+
|
113
|
+
def calculate_trend(self) -> Dict[str, Any]:
|
114
|
+
"""Calculate trends from iteration history.
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
Dict with trend information
|
118
|
+
"""
|
119
|
+
if len(self.history) < 2:
|
120
|
+
return {}
|
121
|
+
|
122
|
+
trends = {
|
123
|
+
"iteration_times": [],
|
124
|
+
"numeric_trends": {},
|
125
|
+
}
|
126
|
+
|
127
|
+
# Calculate iteration times
|
128
|
+
for i in range(1, len(self.history)):
|
129
|
+
trends["iteration_times"].append(self.history[i]["iteration_time"])
|
130
|
+
|
131
|
+
# Find numeric values and calculate trends
|
132
|
+
all_keys = set()
|
133
|
+
for entry in self.history:
|
134
|
+
all_keys.update(self._extract_numeric_keys(entry["results"]))
|
135
|
+
|
136
|
+
for key in all_keys:
|
137
|
+
values = []
|
138
|
+
for entry in self.history:
|
139
|
+
value = self._extract_value(entry["results"], key)
|
140
|
+
if value is not None and isinstance(value, (int, float)):
|
141
|
+
values.append(value)
|
142
|
+
|
143
|
+
if len(values) >= 2:
|
144
|
+
# Calculate simple trend metrics
|
145
|
+
trends["numeric_trends"][key] = {
|
146
|
+
"values": values,
|
147
|
+
"latest": values[-1],
|
148
|
+
"previous": values[-2],
|
149
|
+
"change": values[-1] - values[-2],
|
150
|
+
"change_percent": (
|
151
|
+
(values[-1] - values[-2]) / abs(values[-2]) * 100
|
152
|
+
if values[-2] != 0
|
153
|
+
else 0
|
154
|
+
),
|
155
|
+
"average": sum(values) / len(values),
|
156
|
+
"min": min(values),
|
157
|
+
"max": max(values),
|
158
|
+
}
|
159
|
+
|
160
|
+
return trends
|
161
|
+
|
162
|
+
def _extract_numeric_keys(self, obj: Any, prefix: str = "") -> List[str]:
|
163
|
+
"""Extract all numeric value keys from nested dict."""
|
164
|
+
keys = []
|
165
|
+
|
166
|
+
if isinstance(obj, dict):
|
167
|
+
for key, value in obj.items():
|
168
|
+
full_key = f"{prefix}.{key}" if prefix else key
|
169
|
+
if isinstance(value, (int, float)):
|
170
|
+
keys.append(full_key)
|
171
|
+
elif isinstance(value, dict):
|
172
|
+
keys.extend(self._extract_numeric_keys(value, full_key))
|
173
|
+
|
174
|
+
return keys
|
175
|
+
|
176
|
+
def _extract_value(self, obj: Any, key_path: str) -> Any:
|
177
|
+
"""Extract value from nested dict using dot notation."""
|
178
|
+
keys = key_path.split(".")
|
179
|
+
value = obj
|
180
|
+
|
181
|
+
for key in keys:
|
182
|
+
if isinstance(value, dict) and key in value:
|
183
|
+
value = value[key]
|
184
|
+
else:
|
185
|
+
return None
|
186
|
+
|
187
|
+
return value
|
188
|
+
|
189
|
+
def get_summary(self) -> Dict[str, Any]:
|
190
|
+
"""Get summary of cycle execution.
|
191
|
+
|
192
|
+
Returns:
|
193
|
+
Dict with summary statistics
|
194
|
+
"""
|
195
|
+
summary = {
|
196
|
+
"cycle_id": self.cycle_id,
|
197
|
+
"iterations": self.iteration,
|
198
|
+
"elapsed_time": self.elapsed_time,
|
199
|
+
"start_time": datetime.fromtimestamp(
|
200
|
+
self.start_time, timezone.utc
|
201
|
+
).isoformat(),
|
202
|
+
"history_length": len(self.history),
|
203
|
+
}
|
204
|
+
|
205
|
+
if self.history:
|
206
|
+
summary["first_iteration"] = self.history[0]["timestamp"]
|
207
|
+
summary["last_iteration"] = self.history[-1]["timestamp"]
|
208
|
+
summary["average_iteration_time"] = sum(
|
209
|
+
h.get("iteration_time", 0) for h in self.history[1:]
|
210
|
+
) / max(1, len(self.history) - 1)
|
211
|
+
|
212
|
+
# Add trend summary if available
|
213
|
+
trends = self.calculate_trend()
|
214
|
+
if trends.get("numeric_trends"):
|
215
|
+
summary["trends"] = {
|
216
|
+
key: {
|
217
|
+
"latest": data["latest"],
|
218
|
+
"change": data["change"],
|
219
|
+
"change_percent": data["change_percent"],
|
220
|
+
}
|
221
|
+
for key, data in trends["numeric_trends"].items()
|
222
|
+
}
|
223
|
+
|
224
|
+
return summary
|
225
|
+
|
226
|
+
def to_dict(self) -> Dict[str, Any]:
|
227
|
+
"""Serialize state to dictionary.
|
228
|
+
|
229
|
+
Returns:
|
230
|
+
Dict representation of state
|
231
|
+
"""
|
232
|
+
return {
|
233
|
+
"cycle_id": self.cycle_id,
|
234
|
+
"iteration": self.iteration,
|
235
|
+
"history": self.history,
|
236
|
+
"metadata": self.metadata,
|
237
|
+
"start_time": self.start_time,
|
238
|
+
"last_update_time": self.last_update_time,
|
239
|
+
"node_states": self.node_states,
|
240
|
+
}
|
241
|
+
|
242
|
+
@classmethod
|
243
|
+
def from_dict(cls, data: Dict[str, Any]) -> "CycleState":
|
244
|
+
"""Create CycleState from dictionary.
|
245
|
+
|
246
|
+
Args:
|
247
|
+
data: Dictionary representation
|
248
|
+
|
249
|
+
Returns:
|
250
|
+
CycleState instance
|
251
|
+
"""
|
252
|
+
state = cls(data.get("cycle_id", "default"))
|
253
|
+
state.iteration = data.get("iteration", 0)
|
254
|
+
state.history = data.get("history", [])
|
255
|
+
state.metadata = data.get("metadata", {})
|
256
|
+
state.start_time = data.get("start_time", time.time())
|
257
|
+
state.last_update_time = data.get("last_update_time", state.start_time)
|
258
|
+
state.node_states = data.get("node_states", {})
|
259
|
+
return state
|
260
|
+
|
261
|
+
|
262
|
+
class CycleStateManager:
|
263
|
+
"""Manages multiple cycle states for nested cycles."""
|
264
|
+
|
265
|
+
def __init__(self):
|
266
|
+
"""Initialize cycle state manager."""
|
267
|
+
self.states: Dict[str, CycleState] = {}
|
268
|
+
self.active_cycles: List[str] = []
|
269
|
+
|
270
|
+
def get_or_create_state(self, cycle_id: str) -> CycleState:
|
271
|
+
"""Get existing state or create new one.
|
272
|
+
|
273
|
+
Args:
|
274
|
+
cycle_id: Cycle identifier
|
275
|
+
|
276
|
+
Returns:
|
277
|
+
CycleState instance
|
278
|
+
"""
|
279
|
+
if cycle_id not in self.states:
|
280
|
+
self.states[cycle_id] = CycleState(cycle_id)
|
281
|
+
logger.info(f"Created new cycle state for: {cycle_id}")
|
282
|
+
|
283
|
+
return self.states[cycle_id]
|
284
|
+
|
285
|
+
def push_cycle(self, cycle_id: str) -> None:
|
286
|
+
"""Push cycle onto active stack (for nested cycles).
|
287
|
+
|
288
|
+
Args:
|
289
|
+
cycle_id: Cycle identifier
|
290
|
+
"""
|
291
|
+
self.active_cycles.append(cycle_id)
|
292
|
+
logger.debug(f"Pushed cycle: {cycle_id}, stack: {self.active_cycles}")
|
293
|
+
|
294
|
+
def pop_cycle(self) -> Optional[str]:
|
295
|
+
"""Pop cycle from active stack.
|
296
|
+
|
297
|
+
Returns:
|
298
|
+
Popped cycle ID or None
|
299
|
+
"""
|
300
|
+
if self.active_cycles:
|
301
|
+
cycle_id = self.active_cycles.pop()
|
302
|
+
logger.debug(f"Popped cycle: {cycle_id}, stack: {self.active_cycles}")
|
303
|
+
return cycle_id
|
304
|
+
return None
|
305
|
+
|
306
|
+
def get_active_cycle(self) -> Optional[str]:
|
307
|
+
"""Get currently active cycle ID.
|
308
|
+
|
309
|
+
Returns:
|
310
|
+
Active cycle ID or None
|
311
|
+
"""
|
312
|
+
return self.active_cycles[-1] if self.active_cycles else None
|
313
|
+
|
314
|
+
def get_all_summaries(self) -> Dict[str, Dict[str, Any]]:
|
315
|
+
"""Get summaries for all cycles.
|
316
|
+
|
317
|
+
Returns:
|
318
|
+
Dict mapping cycle_id to summary
|
319
|
+
"""
|
320
|
+
return {
|
321
|
+
cycle_id: state.get_summary() for cycle_id, state in self.states.items()
|
322
|
+
}
|
323
|
+
|
324
|
+
def clear(self, cycle_id: Optional[str] = None) -> None:
|
325
|
+
"""Clear cycle state(s).
|
326
|
+
|
327
|
+
Args:
|
328
|
+
cycle_id: Specific cycle to clear, or None to clear all
|
329
|
+
"""
|
330
|
+
if cycle_id:
|
331
|
+
if cycle_id in self.states:
|
332
|
+
del self.states[cycle_id]
|
333
|
+
self.active_cycles = [c for c in self.active_cycles if c != cycle_id]
|
334
|
+
logger.info(f"Cleared cycle state: {cycle_id}")
|
335
|
+
else:
|
336
|
+
self.states.clear()
|
337
|
+
self.active_cycles.clear()
|
338
|
+
logger.info("Cleared all cycle states")
|